下载 App
10/16案例分享
【案例分享】会 three.js 就能直接写:程序化几何体的两条出图路径与 7 处差异
投稿方向:星火编辑器论坛「案例分享」专区 / 生态贡献度
类型:图文案例
素材来源:《别停下》开发实录 —— 引擎侧生成顶点,Lua 只传参
一句话结论
这套 API 的类名、参数顺序、默认值、语义跟 three.js 完全一致,你把 new THREE.BoxGeometry(1,1,1) 的记忆直接搬过来写 BoxGeometry(1, 1, 1),出图就是对的;真正要记的只有 7 处差异,而它们全都在你不看的时候安静地不报错。
做《别停下》的关卡白模时,我第一次没找美术要任何资源,一个下午搭出了整条跑道——台阶、管道、栏杆、路障、拱门,全是 Lua 现算的。
一、先解决「形状从哪来」
引擎自带 7 个基础模型:Box、Sphere、Cylinder、Cone、Plane、Pyramid、Torus。够搭方块世界,不够搭别的。
球不够圆、环太扁、要个胶囊体没有、要个圆角盒没有。过去的办法是手写 CustomGeometry 循环:自己算顶点、算法线、算 UV,还要自己保证绕序正确。我写过一个半球,光调绕序就花了半小时,正面朝里的时候光照是反的,看起来像内壁。
现在这套 API 把这些全接管了。引擎在底层生成顶点,Lua 只传参。 参数化形体(Box/Sphere/Cylinder/Torus/TorusKnot 这类只收标量参数的)连正面朝向和绕序都不用管,引擎按 three.js 的渲染效果兜底。
二、两条出图路径
每个 XxxGeometry(...) 返回一个 geometry 对象。它不是网格,得再走一步才能进场景。
路径 A:ToModel() → StaticModel.model
静态的、要复用的情况走这条。
-- 先备一个材质(纯色 PBR)
local mat = Material:new()
mat:SetTechnique(0, cache:GetResource("Technique", "Techniques/PBR/PBRNoTexture.xml"))
mat:SetShaderParameter("MatDiffColor", Variant(Color(0.8, 0.3, 0.2, 1.0)))
mat:SetShaderParameter("Metallic", Variant(0.0))
mat:SetShaderParameter("Roughness", Variant(0.5))
-- 出图
local node = scene_:CreateChild("Box")
local obj = node:CreateComponent("StaticModel")
obj.model = BoxGeometry(1, 1, 1):ToModel()
obj.material = mat
ToModel() 出来的 Model 可以挂在多个节点上,只占一份显存:
local shared = BoxGeometry(1, 1, 1):ToModel()
for i = 1, 10 do
local n = scene_:CreateChild("b" .. i)
n.position = Vector3(i * 1.5, 0, 0)
n:CreateComponent("StaticModel").model = shared
end
我关卡里的 40 个路障就是这么来的:一次 ToModel(),40 个节点。顶点只上传一次。
路径 B:FillCustomGeometry(cg)
形状每帧都在变的时候走这条——地形起伏、波形、被打碎的碎片。
local cg = node:CreateComponent("CustomGeometry")
SphereGeometry(1, 32, 16):FillCustomGeometry(cg)
cg:SetMaterial(mat)
要在运行时反复重建,记得开动态:
local geom = TorusKnotGeometry(0.4, 0.12, 64, 8)
geom:SetDynamic(true) -- 每帧重建必须开
geom:FillCustomGeometry(cg)
怎么选
情况:走哪条
形状固定,多个节点共用:A,ToModel() 一次,挂 N 次
形状固定,只用一次:两条都行,A 少写一行
形状每帧变:B + SetDynamic(true)
想拿包围盒做碰撞:A,model.boundingBox 直接读
三、与 three.js 的差异:全部 7 处
这张表是全文最该抄走的部分。会 three.js 的人只需要看这 7 行。
项:three.js 这边
顶点数据怎么拿:读 geometry.attributes 没有 attributes,用 :ToModel() / :FillCustomGeometry(cg) 出图
数组类参数:Vector2 / Vector3 对象数组 Lua 表:{ Vector2(..), Vector3(..) }
ParametricGeometry 的曲面函数:传 JS 函数 传字符串名:ParametricGeometry("Klein", 25, 25),内置 "Klein" "Plane" "Mobius" "Mobius3D"
线几何的材质:无要求 EdgesGeometry / WireframeGeometry / BoxLineGeometry 无法线,材质必须用无光照 Techniques/NoTextureUnlit.xml
ConvexGeometry 的 UV:不产 UV 额外产 UV,逐面平面投影,1 UV 单位 = 1 世界单位
暂不支持:— ExtrudeGeometry 无 bevel;ShapeGeometry / ExtrudeGeometry 不支持带洞轮廓;TextGeometry 不可用
扫掠弧段方位:按 three.js 坐标推算 thetaStart / phiStart 弧段与 three.js 渲染效果一致,但别按引擎世界坐标推算它落在哪一侧
逐条说几个。
数组参数用 Lua 表。 没有 new THREE.Vector2() 这回事,直接 Vector2(x, y) 塞进表里:
LatheGeometry({ Vector2(0, -0.5), Vector2(0.4, 0), Vector2(0, 0.5) }, 24)
TubeGeometry({ Vector3(-2, 0, 0), Vector3(0, 1, 1), Vector3(2, 0, 0) }, 64, 0.2, 8)
表里的坐标逐字生效,不会被镜像。 这条单独拎出来讲,因为它划出了两种形体的行为分界:
- 参数化形体(只传标量)→ 引擎自动兜底坐标系,你写 three.js 的参数就行;
- 显式坐标输入(点云、路径点、顶点表、截面、2D 轮廓、贴花位置)→ 你给的坐标就是引擎世界坐标。
ExtrudeGeometry 沿 +Z 挤出 depth,实体占 z ∈ [0, depth],不居中;ShapeGeometry 的面朝 +Z。
ConvexGeometry 多产 UV 是故意的。 three.js 里它不产 UV,带法线贴图的材质挂上去就是坏的。这边逐面做了平面投影,1 UV 单位 = 1 世界单位,纹素密度全网格一致。贴图会按面各自平铺——看到接缝别当 bug 修。
TextGeometry 现在拿不到。 我试过 TextGeometry("别停下", ...),返回 nil,不报错。要用文字轮廓,先把字形转成顶点表,再走 ExtrudeGeometry。
四、可用几何体清单
参数同 three.js 同名构造函数,我只列名。
Core(21 个)
BoxGeometry PlaneGeometry SphereGeometry CylinderGeometry ConeGeometry CircleGeometry RingGeometry TorusGeometry TorusKnotGeometry TubeGeometry LatheGeometry CapsuleGeometry TetrahedronGeometry OctahedronGeometry IcosahedronGeometry DodecahedronGeometry PolyhedronGeometry ShapeGeometry ExtrudeGeometry EdgesGeometry WireframeGeometry
Addons(7 个)
RoundedBoxGeometry BoxLineGeometry ParametricGeometry ConvexGeometry LoftGeometry DecalGeometry TeapotGeometry
忘了参数?回忆 three.js 同名构造函数就行,一模一样:
CylinderGeometry(radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength)
几个高频的:
CapsuleGeometry(0.5, 1.0):ToModel() -- 胶囊:角色占位、药丸
RoundedBoxGeometry(1, 1, 1, 4, 0.15):ToModel() -- 圆角盒:UI 面板、道具
收表的几何体
LatheGeometry({ Vector2(0,-0.5), Vector2(0.4,0), Vector2(0,0.5) }, 24) -- 2D 剖面绕 Y 轴旋转
TubeGeometry({ Vector3(-2,0,0), Vector3(0,1,1), Vector3(2,0,0) }, 64, 0.2, 8) -- 路径点(Catmull-Rom)
ConvexGeometry({ Vector3(...), Vector3(...), Vector3(...) }) -- 点云凸包
ShapeGeometry({ Vector2(...), ... }) -- 2D 轮廓 → 平面片
ExtrudeGeometry({ Vector2(...), ... }, 0.5) -- 2D 轮廓 → 立体,depth=0.5
PolyhedronGeometry({ x,y,z, ... }, { i0,i1,i2, ... }, 1, 0) -- 顶点/索引:扁平数字表,索引 0 基
PolyhedronGeometry 两个坑叠在一起:顶点和索引都是扁平数字表,不是嵌套;索引是 0 基。而且三角形索引必须按外向绕序给——面法线 (v1-v0)×(v2-v0) 指向实体外部,这个绕序引擎不帮你归一化。
ShapeGeometry / ExtrudeGeometry 的 2D 轮廓倒是顺逆时针都能写,内部自动归一化,跟 three.js 一样。
派生几何体
输入是另一个 geometry 对象:
local box = BoxGeometry(1, 1, 1, 2, 2, 2)
WireframeGeometry(box):ToModel() -- 全部三角边
EdgesGeometry(box, 1):ToModel() -- 只保留硬边,第二参是阈值角度(度)
DecalGeometry(SphereGeometry(1.3), Vector3(0,0,1.3), Quaternion(), Vector3(1,1,3)) -- 贴花投影到 mesh
EdgesGeometry 的第二个参数是度,不是弧度。我一开始传 0.1,想要 0.1 弧度的阈值,结果得到一张几乎全保留的线框——0.1 度比 0.1 弧度小五十多倍。
对象方法
:ToModel() · :FillCustomGeometry(cg) · :GetVertexCount() · :GetIndexCount() · :GetBoundingBox() · :SetDynamic(true)
五、踩过的坑
坑 1:法线贴图整片发黑。
最贵的一个。场景里所有材质都正常,一换带法线贴图的就黑成一片,控制台一个字没有。
原因:法线贴图在切线空间采样,需要每个顶点带切线(tangent)。手写 CustomGeometry 的路径不自动算切线。
-- ❌ 手写路径,Commit 之后法线贴图材质整片黑
geom:BeginGeometry(0, TRIANGLE_LIST)
-- ... DefineVertex / DefineNormal / DefineTexCoord ...
geom:Commit()
-- ✅ Commit 之前补一刀
geom:BeginGeometry(0, TRIANGLE_LIST)
-- ... 同上 ...
geom:GenerateTangents() -- 必须在 Commit 前
geom:Commit()
这套 three.js 兼容 API 默认就带切线,ToModel() 出来的 Model 直接挂法线贴图材质就行。这也是我从手写路径迁过来的直接原因。
坑 2:线几何配了光照材质。
-- ❌ 线没有法线,光照材质算出来是黑的
local mat = Material:new()
mat:SetTechnique(0, cache:GetResource("Technique", "Techniques/PBR/PBRNoTexture.xml"))
EdgesGeometry(box, 30):ToModel() -- 挂上去 → 看不见
-- ✅ 无光照
mat:SetTechnique(0, cache:GetResource("Technique", "Techniques/NoTextureUnlit.xml"))
EdgesGeometry / WireframeGeometry / BoxLineGeometry 三个都不产法线。做线框、描边、调试网格,材质一律用 NoTextureUnlit.xml。
坑 3:给 ParametricGeometry 传了个函数。
-- ❌ Lua 里没有 JS 函数回调这一套
ParametricGeometry(function(u, v) return Vector3(u, v, 0) end, 25, 25)
-- ✅ 传字符串名,内置四个
ParametricGeometry("Klein", 25, 25)
ParametricGeometry("Mobius3D", 32, 32)
内置只有 "Klein" "Plane" "Mobius" "Mobius3D" 四个。想画自己的参数曲面,回到 CustomGeometry 手写。
坑 4:按 three.js 的方位直觉推算弧段。
thetaStart / phiStart 切出来的弧段,渲染效果跟 three.js 一致——但别拿引擎世界坐标去反推它落在哪一侧。
我做过一段 90° 的弧形护栏,按坐标算好了开口朝 −X,渲出来朝另一边。要精确方位就生成完再转节点:
local n = scene_:CreateChild("arc")
n:CreateComponent("StaticModel").model =
CylinderGeometry(1, 1, 0.2, 24, 1, true, 0, math.pi / 2):ToModel()
n.rotation = Quaternion(0, 90, 0) -- 用节点旋转补方位,别跟参数较劲
坑 5:ExtrudeGeometry 挤出的模型埋进地里。
它沿 +Z 从 0 挤到 depth,不居中。
-- ❌ 以为居中,半个模型在地面下
node.position = Vector3(0, 0, 0)
node:CreateComponent("StaticModel").model =
ExtrudeGeometry({ Vector2(-0.5,-0.5), Vector2(0.5,-0.5), Vector2(0.5,0.5), Vector2(-0.5,0.5) }, 0.4):ToModel()
-- ✅ 补半个 depth
node.position = Vector3(0, 0.2, 0) -- 0.4 / 2
坑 6:以为 ConvexGeometry 的接缝是 UV 坏了。
点云凸包贴木纹,每个面上的纹理方向都不一样,接缝明显。这是逐面平面投影的必然结果——纹素密度一致,代价是方向不连续。想要连续 UV 得回到 CustomGeometry 自己展开。
坑 7:从 Primitives 那套迁移时,把顶点色一起搬过来。
urhox-libs 里旧的 Primitives 库每个函数都收 color / outerColor / innerColor,逐顶点写颜色,配顶点色 technique。新 API 不产生顶点色。
-- ❌ 新 API 没有颜色参数,写了也不生效
CapsuleGeometry(0.5, 1.0, 4, 8, "red")
-- ✅ 颜色走材质
obj.model = CapsuleGeometry(0.5, 1.0):ToModel()
obj.material = redMat
同一个形体里要多种颜色,就得拆成多个几何体,或者继续手写 CustomGeometry。
顺带一句:旧的 Primitives 库(Hemisphere / Disc / Arc / Cone / Torus)在 2026-08 已弃用,新代码别再用。它留在库里是因为线上老游戏快照的是引用不是本体,删掉会让 require 直接失败。
坑 8:以为分段数可以省。
SphereGeometry(1, 32, 16) 是 32×16 段。参数化形体的面数完全由分段数决定,你没法减面、没法做 LOD。一个场景塞 200 个 32×16 的球,移动端就顶不住了。
SphereGeometry(1, 32, 16) -- 桌面:主角、近景物体
SphereGeometry(1, 12, 8) -- 移动端:远处装饰、粒子替代
六、什么时候用这套,什么时候该找美术
用这套:
- 关卡白模、碰撞体占位、原型验证——不用等美术,一个下午能跑起来;
- 几何拼装:台阶、栏杆、管道、货架、路障、拱门。这类东西用参数化形体拼比建模快,改个数字就换尺寸;
- 程序化生成:随机星球、碎片、地形装饰、按种子长的结构;
- 运行时改参数的:半径、分段数、扭转圈数;
- 内置模型里没有的形状:胶囊、圆角盒、多面体、星形挤出、凸包、贴花。
该找美术:
- 角色、生物、任何要骨骼动画的东西——这套 API 不产骨骼、不产蒙皮权重;
- 需要精心展开 UV 的贴图资产——参数化形体的 UV 是自动算的,接缝位置没法定制;
- 面数敏感的场景——面数只由分段数决定,减不了面,也没有 LOD 链;
- 需要顶点色的——上面坑 7 说过,新 API 不产生顶点色。
一句话分界:形状能用数字描述清楚的,用代码;说不清楚的,交给建模。
七、一条可迁移的判断方法
以后遇到「这套 API 长得像另一个引擎的 API」的移植场景,别从文档第一页开始读。
先跑一个能看见的最小例子,再拿差异表逐条对。
差异表只列我踩过的坑。没列出来的,默认就是一致的——这个默认值值得信,因为作者填这张表的动机就是「这里跟你想的不一样」。
而跑一次最小例子,能一次性把坐标系方位、绕序、默认值三件事全暴露出来。这三个恰好是差异表最容易漏、也最容易让你怀疑人生的地方。
我这套几何体 API 的上手顺序就是这样:写一行 BoxGeometry(1, 1, 1):ToModel() 挂进场景看见一个方块,确认坐标系没翻;换成 ExtrudeGeometry 确认挤出方向;再挂一个带法线贴图的材质确认切线在。三步跑完,剩下的就是查表。
(完)
星火编辑器-新手教程-关于多游戏模式的配置和触发的获取截图
星火编辑器-新手教程-关于多游戏模式的配置和触发的获取
HELLO 大家好,我是阿木木牧。最近第一版的内部小范围(几人)第一个图的测试已经结束了。 整体游戏以装备数值的崩坏而告终~ (虽然大家都很欢快。。。) 现在准备开始开发下个阶段的内容啦,就是将游戏模式拆分出来。分成单人模式,多人组队模式(合作PVE),多人匹配模式(对抗模式)。那么,怎么配置呢?说来也很简单哈~ 1,在游戏编辑器中,数据编辑器,找到地图设置。 2,编辑地图设置。比如我希望最大数量
14 赞
3 回复
00:19
如何实现单位的随意拖拽摆放?【星火编辑器教程】截图
如何实现单位的随意拖拽摆放?【星火编辑器教程】
很多小伙伴想做自走棋类的游戏,那么就会涉及到单位的一个最基础的操作,就是选中单位并拖拽和摆放,那如何实现单位的拖拽摆放呢,今天就来教大家如何实现这样的功能。实现效果如下视频所示。 教程开始 第一步: 打开编辑器的触发编辑器,新建一个文件夹(我就叫“拖拽功能”,方便对拖拽功能的逻辑进行归类,如果你不建文件夹也没关系,习惯而已) 第二步: 点击文件夹右边的+号,添加一个变量,我这里叫“变量_目前选中的
19 赞
12 回复
无须申请,星火编辑器现已公测开放下载截图
无须申请,星火编辑器现已公测开放下载
—————————————————— 各位关注星火编辑器的小伙伴: 感谢大家一直以来对编辑器支持! 破晓已去,曙光降临! 星火编辑器现已开启公测! · 关于下载和资格申请 现已无需申请,请去下面的官方首页直接下载! 缘起测试是星火编辑器自诞生以来第一次对外测试! 从2022年八月开始到十一月,历时三个月。 缘起之后缘聚测试,历时又七个月! 缘聚之后,破晓降临,历时又十二个月! 曾经微弱的星火现已渐
官方
331 赞
375 回复