本节摘要:真实项目的网格有两个来源——程序化拼装与外部模型文件,两种来源都绕不开"复用"这道选择题:合并、实例化还是克隆,三者决定第6站的绘制调用数量。本节给出三种手段的机理对比与选型结论,并走完 glTF 模型从加载到挂进场景树的完整流程。承接 2.3 的数据结构,通向第5章的绘制调用优化。
一栋楼有五百个构件,五百个网格就是五百次绘制调用。合并手段把多个网格的顶点数据拼进同一个缓冲,五百次调用变一次。代价同样清楚:合并后整体是一个包围盒,剔除粒度变粗;原始网格的身份消失,无法再单独移动其中任何一件。适合"形状各异但永远同进同退"的静态集合——建筑外壳、地形装饰、一次焊死的家具。
// 把四栋楼的构件合并成一个网格:绘制调用从几百降到一 const parts = [wallMesh, roofMesh, doorMesh, windowMesh]; const building = BABYLON.Mesh.MergeMeshes( parts, true, // 允许合并时移动网格到同一坐标系 true, // 合并后共享同一材质:想合并省调用就必须共享材质 undefined, false, // 不保留源网格 true // 多材质时按材质分组分别合并 ); building.freezeWorldMatrix(); // 静态合并件顺手冻结,第5章展开
城市绿化要摆两千棵同一款树。克隆两千份顶点数据,显存直接乘两千;实例化让两千个网格共享同一份顶点缓冲,每个实例只存自己的变换——两 KB 换两百万顶点,这是引擎里性价比最高的复用手段。GPU 绘制时按实例逐个摆放,绘制调用的节省虽不及合并彻底(不同硬件实现有差异),显存与上传带宽的节省却是实打实的。

实例化的代码极短,收益却立竿见影:
// 一棵母树,两千个实例:共享顶点,各摆各的位置 const treeProto = BABYLON.MeshBuilder.CreateCylinder("tree", { height: 3, diameterTop: 0, diameterBottom: 0.6 }, scene); const treeTop = BABYLON.MeshBuilder.CreateSphere("crown", { diameter: 2 }, scene); treeTop.position.y = 2.2; treeTop.parent = treeProto; for (let i = 0; i < 2000; i++) { const t = treeProto.createInstance("tree" + i); // 实例只带变换,不带数据 t.position.set((Math.random() - 0.5) * 200, 0, (Math.random() - 0.5) * 200); t.rotation.y = Math.random() * Math.PI * 2; // 转个随机朝向,破除阵列感 t.scaling.setAll(0.8 + Math.random() * 0.4); // 大小微差 }
实例的局限要记住:它没有自己的材质与几何,想换贴图就得回到克隆;拾取倒是正常支持的,第4章会用到这一点。
程序化拼装适合几何规则的东西,复杂资产来自美术的建模软件,交换格式首选 glTF——它按"面向 GPU 的数据结构"设计,顶点、索引、材质、节点树一一对应引擎概念,加载后无需转换即可直接消费。
装载分两步:异步加载,异步回调里接树:
// 异步加载模型:回调里拿到的是容器,不是网格本身 BABYLON.SceneLoader.ImportMeshAsync( "", // 目录名留空,用完整地址 "assets/", // 资源目录 "robot.glb", // 文件名:glb 是 glTF 的二进制打包 scene ).then((result) => { const root = result.meshes[0]; // 模型根节点:挂靠点 root.position.set(0, 0, 0); root.scaling.setAll(2); // 美术单位与场景单位经常不一致,先统一比例 // 遍历模型自带节点树:结构与 2.1 的场景树同构 for (const m of result.meshes) { console.log(m.name, m.getTotalVertices()); } // 模型内部父子关系原样保留:动画骨骼、部件联动都直接可用 }).catch((err) => console.error("加载失败", err));
加载期有两件事值得做在前面。第一,预热:模型首次出现在画面里时才编译材质、上传纹理,会造成 1.2 节说过的帧率尖刺,加载后先渲染一帧再进正式场景可以消化掉。第二,清点:加载完立刻打印顶点总数与材质数量,超预算的模型退回美术减面,比上线后优化便宜十倍。
⚠️ 常见坑:坐标轴约定不同——有的建模软件竖轴是 Z,引擎竖轴是 Y,模型进来看似"趴着";别急着旋转每个部件,转根节点一次到位。另一个坑是忘了处理加载失败分支,弱网下场景空转还以为代码错了。
合并之后又想单独动其中一件怎么办? 合并不可逆,源网格已经销毁。正确姿势是合并前就把"可能单独动"的那件从名单里拿出来,或者按变动粒度分组分别合并——"按街区合并"的口诀正是这个意思。
实例支持拾取吗? 支持。实例有自己的变换与标识,4.2 节的射线查询能精确到具体实例;但它改不了材质与几何,要逐个换装的部分请回到克隆。
大模型加载能给进度条吗? 异步加载的进度回调可以驱动进度条。更稳的做法是把大模型拆成若干部件文件按需加载、先到先显,观感好过一根卡在八成的进度条,也顺带摊平了纹理上传的尖刺。
第2章到此,流水线前四站的实体全部就位。下一章进入第5到7站:材质与着色——数据要变成颜色了。