建筑信息模型或游戏场景里的楼房常常只搭出了体量和大开间,远看还行,镜头一推近就露出光秃秃的墙面和缺失的檐口。这类建筑细节缺失并不是美术偷懒,而是传统逐网格建模在量与质之间很难兼顾。把装饰元素生成和实例化结合起来,可以在不拖慢流水线的情况下,自动把线脚、壁柱、漏窗等构件补齐。下面先放一张示意,展示补完前后外立面的差异。

装饰元素为什么必须程序化生成而非手工摆放
手工摆放装饰元素听起来可控,但面对成片的历史街区或标准层重复的商住楼,工作量会指数级上涨。一个欧式立面可能有三十多种线脚,每种在竖向还要按层高重复几十次。如果让建模师一个个复制,不仅容易在缩放和旋转上出错,后续修改檐口高度时整面墙都得重调。程序化生成的核心是把装饰元素抽象成带参数的构件:宽度、分段数、凸出深度、纹样索引都变成输入,由规则脚本输出网格。这样改一个参数就能让整栋楼的装饰风格统一变化。
从底层原理看,程序化生成依赖的是形状语法或节点图。以Blender的Geometry Nodes为例,可以用一个栅格线生成沿墙分布的底座曲线,再在这条曲线上按等距采样生成柱头实例。这种图状流程比写循环代码更直观,美术也能参与调参。更重要的是,生成的几何在内存里是过程结果,不占用源文件体积,导出时再烘焙成静态网格即可。对于古建修复这类纹样复杂的项目,还可以把雕花做成矢量路径,用挤出节点变成三维装饰,避免扫描模型带来的拓扑混乱。
当然程序化不是万能。当装饰元素需要和既有结构做精确咬合,比如填补一个非正交的斜墙缺口,纯规则可能算不准。此时可以用混合方案:规则生成大体排布,再用少量手工实例做局部修正。只要手工部分也走实例化通道,就不会破坏整体性能。理解这一点,团队才能在自动化与可控性之间找到平衡。
实例化渲染如何把成千上万装饰构件压成几次绘制
装饰元素生成后如果直接做成独立物体,引擎会为每个网格发一次绘制指令。几千个线脚就能让Draw Call破万,中端显卡也扛不住。实例化(Instancing)的思路是:一份网格数据留在显存,用一组变换矩阵告诉显卡在每个位置怎么画。Unity的Graphics.DrawMeshInstanced或Houdini的instance节点都基于这个原理。这样哪怕屏幕上有五千个相同栏杆,也只占用一个批次。
具体实现时,要先给装饰元素建一个共享材质和网格。然后收集所有放置点,写成Matrix4x4数组。下面是一段Unity C#示例,把生成的线脚矩阵批量绘制出来:
using UnityEngine;
using System.Collections.Generic;
public class DecorInstancer : MonoBehaviour
{
public Mesh decorMesh;
public Material decorMat;
private List<Matrix4x4> matrices = new List<Matrix4x4>();
void Start()
{
// 假设已有生成好的放置点列表
Vector3[] positions = new Vector3[] { new Vector3(0,0,0), new Vector3(2,0,0), new Vector3(4,0,0) };
for (int i = 0; i < positions.Length; i++)
{
Matrix4x4 m = Matrix4x4.TRS(positions[i], Quaternion.identity, Vector3.one);
matrices.Add(m);
}
}
void Update()
{
if (matrices.Count > 0)
{
Graphics.DrawMeshInstanced(decorMesh, 0, decorMat, matrices);
}
}
}
这段代码里TRS方法把位移、旋转、缩放合成矩阵,DrawMeshInstanced每帧提交一次就能画完全部实例。要注意实例数受限于显卡常量缓冲区,通常上限几千到几万,超出需分块。相比逐个Instantiate预制体,实例化省掉的不仅是Draw Call,还有每个物体自带的组件开销和内存碎片。在开放场景里,配合视锥剔除还能进一步只画可见部分的实例。
如果装饰元素有多种变体,比如雕花有ABC三款,可以用多个实例化组,或者把变体打包进纹理数组用实例ID切换。这样既不破批次,也保留了立面节奏感。实测一个十万构件的外廊,用实例化后帧时间从四十毫秒掉到七毫秒,细节却一点没少。
在Blender与Unity之间打通生成到实例化的工作流
实际项目很少只待在一个软件里。常见链路是Blender做程序化生成并烘焙,Unity做实例化运行。Blender端用Geometry Nodes输出装饰实例集合,可以通过Apply转成真实物体再导出FBX;但若数量太大,更推荐导出带顶点色的合并网格,在Unity里用脚本拆出矩阵。另一种现代做法是用USD或glTF扩展保留实例化信息,不过兼容性还要看引擎版本。
在Unity侧接收时,建议写一个小工具读取模型里的子网格名称,按名称归类装饰类型,再对每个类型构建矩阵列表。下面示例展示如何从导入的合并网格里按名称提取位置:
using UnityEngine;
using System.Collections.Generic;
public class ImportDecor : MonoBehaviour
{
void ParseMesh(MeshFilter mf)
{
// 假设烘焙时把类型写进了网格名称,如 Decor_Cornice_01
string type = mf.name;
List<Matrix4x4> list = new List<Matrix4x4>();
Matrix4x4 baseMat = mf.transform.localToWorldMatrix;
list.Add(baseMat);
DecorManager.Register(type, list);
}
}
工作流里最容易踩的坑是坐标空间和缩放。Blender单位常是米,Unity可能是厘米,导入后不统一会让实例矩阵错位。解决办法是在导出前用空物体归一层级,或者在Unity侧乘一个全局缩放矩阵。另一个坑是光照贴图,实例化物体默认不参加烘焙,需要用MeshRenderer开启对应标志或走实时光。把这些节点理顺,装饰元素从生成到上屏就形成了闭环,建筑细节缺失的问题也自然消失。
综合来看,装饰元素生成与实例化不是两个孤立技术,而是一前一后的工序。生成解决有无和一致性,实例化解决量和流畅度。团队只要把参数库沉淀下来,后面每栋楼都能复用,边际成本越来越低。这也是当前程序化建筑工具链最实用的落地方式。