thin_Incetance导致的位置偏移问题
做公司的新项目时,需要给场景中的树木都使用thinInstance创建实例节省资源,效果如下,场景中所有的树木其实都是同一个模型所渲染,仅仅只是位置不一样而已
# 什么是 thinInstance?
# 实例化技术基础
在3D渲染中,实例化(Instancing)是一种通过复用单个网格几何体(Geometry)来渲染多个相同物体的技术。想象你要画1000个相同的立方体,如果为每个立方体创建独立网格,会导致大量重复数据(顶点,纹理等)占用内存,且JavaScript引擎需要处理1000个对象,性能开销巨大。
实例化技术通过共享集合体,仅存储每个实例的差异化数据(位置,旋转,缩放)来解决这个问题。
Babylon.js提供了两种实例化方案:
常规实例(InstancedMesh)
每个实例是独立对象,可单独控制可见性,材质等,但创建10000个实例会生成10000个Javascript对象,导致JS引擎卡顿。轻量级实例(thinInstance)
所有实例数据存储在二进制缓冲区(Float32Array)中,不创建独立对象,JavaScript层面零额外开销,适合大量静态/半静态重复物体(如森林,建筑群,粒子效果)。
# thinInstance的核心优势
特性 | InstancedMesh | thinInstance |
---|---|---|
JS对象数量 | 每个实例对应1个对象 | 所有实例共享1个基础网格对象 |
内存占用 | 高(每个对象都有额外属性) | 极低(仅存储矩阵)缓冲区 |
渲染性能 | 中(JS循环遍历开销大) | 极高(GPU直接读取缓冲区) |
动态更新效率 | 高(可单独更新实例) | 中(需重建/部分更新缓冲区) |
适用场景 | 少量动态实例(如敌人,交互物体) | 大量静态实例(如树木,建筑) |
# thinInstance工作原理
# 数据存储方式
thinInstance通过44变换矩阵描述每个实例的位置,旋转,缩放,矩阵数据存储在Float32Array缓冲区中。每个矩阵包含16个浮点数(4行4列),因此N个实例需要16*N长度的缓冲区。
# 渲染流程
- 创建基础网格:定义共享的几何体和材质(如一棵树的模型)
- 隐藏基础网格:基础网格自动隐藏,不会参与渲染
- 填充实例缓冲区:将所有实例的矩阵数据写入Float32Array
- 绑定缓冲区并渲染:通过thinInstanceSetBuffer绑定缓冲区。
注意
注意:thinInstance 是 “全有或全无” 的渲染逻辑 —— 要么所有实例都渲染,要么都不渲染,无法单独隐藏某个实例(需通过矩阵缩放为 0 或 alpha 通道实现 “伪隐藏”)。
# 核心API详解
# 基础方法
# thinInstanceAdd(matrix)
添加单个实例矩阵
const idx = mesh.thinInstanceAdd(BABYLON.Matrix.Translation(1, 2, 3))
# thinInstanceSetBuffer(name, buffer, stride)
设置实例缓冲区(矩阵 / 自定义属性)
mesh.thinInstanceSetBuffer("matrix", matrixBuffer, 16)
# thinInstanceCount
获取 / 设置实例数量
console.log(mesh.thinInstanceCount); // 输出实例总数
# thinInstanceEnablePicking
启用实例拾取(点击交互)
mesh.thinInstanceEnablePicking = true;
# thinInstanceAddSelf()
渲染基础网格自身(作为一个实例)
mesh.thinInstanceAddSelf(); // 基础网格也参与渲染
# 矩阵计算工具
Babylon.js提供BABYLON.Matrix类简化矩阵创建,常用方法:
- 平移矩阵:Matrix.Translation(x,y,z)
- 缩放矩阵:Matrix.Scaling(x,y,z)
- 旋转矩阵:Matrix.RotationYawPitchRoll(yaw, pitch, roll)
- 复合矩阵:Matrix.Compose(scale, rotation, position)(合并缩放,旋转,平移)
# 实战案例
# 案例1:创建1000个随机立方体
// 初始化场景
// 获取画布元素
const canvas = document.getElementById("renderCanvas1");
// 初始化Babylon.js引擎
const engine = new BABYLON.Engine(canvas, true);
// 创建场景
const scene = new BABYLON.Scene(engine);
scene.createDefaultCameraOrLight(true, true, true);
const baseCube = BABYLON.Mesh.CreateBox("base", 1, scene);
baseCube.material = new BABYLON.StandardMaterial("mat", scene);
baseCube.material.diffuseColor = new BABYLON.Color3(1, 0.5, 1); // 基础颜色
const instanceCount = 1000; // 实例数量
// 1. 矩阵缓冲区:存储每个实例的位置、旋转、缩放
const matrixBuffer = new Float32Array(16 * instanceCount);
// 2. 颜色缓冲区:存储每个实例的颜色(自定义属性)
const colorBuffer = new Float32Array(4 * instanceCount); // 每个实例 4 个值(RGBA)
for (let i = 0; i < instanceCount; i++) {
// 随机位置(范围:x=-50~50, y=-5~5, z=-50~50)
const position = new BABYLON.Vector3(
Math.random() * 100 - 50,
Math.random() * 10 - 5,
Math.random() * 100 - 50
);
// 随机旋转(Y 轴旋转 0~360 度)
const rotation = BABYLON.Quaternion.FromEulerAngles(0, Math.random() * Math.PI * 2, 0)
// 随机缩放(0.5~1.5 倍)
const scale = new BABYLON.Vector3(
0.5 + Math.random(),
0.5 + Math.random(),
0.5 + Math.random()
);
// 生成复合矩阵并写入缓冲区
const matrix = BABYLON.Matrix.Compose(scale, rotation, position);
matrix.copyToArray(matrixBuffer, i * 16);
}
// 绑定矩阵缓冲区(必选)
baseCube.thinInstanceSetBuffer("matrix", matrixBuffer, 16);
// 启动渲染循环
engine.runRenderLoop(() => {
scene.render();
});
// 监听窗口大小变化
window.addEventListener("resize", () => {
engine.resize();
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 案例2:文章开头截图案例
之前在做树木实例的时候,位置总是有偏差,其实关键的点如下:
const treePositions = [
{
"id": "PlantFinal.Tree._m",
"position": [
55.74355697631836,
-0.4445801079273224,
369.2821960449219
]
},
{
"id": "PlantFinal.Tree._m_1",
"position": [
85.84656524658203,
-0.4445033669471741,
359.901123046875
]
},
.....
]
SceneLoader.ImportMesh(
null,
"/models/",
"treeH.glb",
scene,
(instanceMeshes) => {
// 获取源模型(第一个是树干,第二个是树叶,这两个模型的中心点都被我放在了树干的底部)
const sourceTree = instanceMeshes[1]
const sourceTree1 = instanceMeshes[2]
const instanceMatrices = getMeshInstanceMeshes(sourceTree, treePositions, 0.4);
const instanceMatrices1 = getMeshInstanceMeshes(sourceTree1, treePositions, 0.4);
// 添加所有Thin Instance
if (instanceMatrices.length > 0) {
sourceTree.thinInstanceAdd(instanceMatrices);
sourceTree1.thinInstanceAdd(instanceMatrices1);
} else {
console.warn("未创建任何实例,请检查树木位置数据");
}
}
);
function getMeshInstanceMeshes(mesh, positions, scale) {
// 强制重置源模型变换
mesh.position.set(0, 0, 0); // 重置位置
mesh.rotation.set(0, 0, 0); // 重置旋转
mesh.scaling.set(scale, scale, scale); // 设置统一缩放
mesh.setParent(null); // 解除父节点影响
mesh.computeWorldMatrix(true); // 立即计算世界矩阵(关键步骤)
// 关键修复:计算源模型的世界矩阵和逆矩阵
const worldMatrix = mesh.getWorldMatrix(); // 获取源模型当前世界矩阵
const invWorldMatrix = new Matrix(); // 创建逆矩阵对象
worldMatrix.invertToRef(invWorldMatrix); // 计算世界矩阵的逆矩阵
// 生成实例矩阵
const instanceMatrices = [];
positions.forEach(item => {
// 创建平移矩阵(基于树木位置数据)
const translationMatrix = Matrix.Translation(
item.position[0],
item.position[1],
item.position[2]
);
// 计算完整实例矩阵:t = worldMatrix * translationMatrix * invWorldMatrix
const tempMatrix = new Matrix();
translationMatrix.multiplyToRef(invWorldMatrix, tempMatrix); // T * invWorld
const finalMatrix = worldMatrix.multiply(tempMatrix); // World * (T * invWorld)
instanceMatrices.push(finalMatrix); // 添加到实例矩阵数组
});
return instanceMatrices // 返回所有实例的矩阵数组
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
- 模型变换重置
mesh.position.set(0, 0, 0);
mesh.rotation.set(0, 0, 0);
mesh.scaling.set(scale, scale, scale);
2
3
- 目的:确保源模型处于“基准状态”,避免自身变换影响实例位置
- 注意:scaling.set(scale)统一缩放模型,避免实例继承错误缩放值
- 世界矩阵与逆矩阵计算
const worldMatrix = mesh.getWorldMatrix();
const invWorldMatrix = new Matrix();
worldMatrix.invertToRef(invWorldMatrix);
2
3
- 世界矩阵(World Matrix):包含模型的位置、旋转、缩放信息的 4x4 矩阵
- 逆矩阵(Inverse Matrix):用于 "抵消" 源模型的变换,使实例位置基于世界坐标系计算
- 为什么需要:若源模型有初始变换(如导出时不在原点),直接使用平移矩阵会导致实例位置偏移
- 实例矩阵计算
// 矩阵乘法顺序:World * (Translation * InverseWorld)
translationMatrix.multiplyToRef(invWorldMatrix, tempMatrix); // T * invW
const finalMatrix = worldMatrix.multiply(tempMatrix); // W * (T * invW)
2
3
数学意义:
- 先用逆矩阵抵消源模型的世界变换(T * invW)
- 再用源模型的世界矩阵恢复整体变换(W * ...)
- 最终效果:实例位置相对于世界坐标系定位,不受源模型初始变换影响
简化理解:
相当于把实例 "粘贴" 到世界坐标系的item.position位置,同时保持源模型的缩放和旋转特性