thin_Incetance导致的位置偏移问题

更新时间: 2025-07-22 08:29:51

做公司的新项目时,需要给场景中的树木都使用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长度的缓冲区。

# 渲染流程

  1. 创建基础网格:定义共享的几何体和材质(如一棵树的模型)
  2. 隐藏基础网格:基础网格自动隐藏,不会参与渲染
  3. 填充实例缓冲区:将所有实例的矩阵数据写入Float32Array
  4. 绑定缓冲区并渲染:通过thinInstanceSetBuffer绑定缓冲区。

注意

注意:thinInstance 是 “全有或全无” 的渲染逻辑 —— 要么所有实例都渲染,要么都不渲染,无法单独隐藏某个实例(需通过矩阵缩放为 0 或 alpha 通道实现 “伪隐藏”)。

# 核心API详解

# 基础方法

# thinInstanceAdd(matrix)

添加单个实例矩阵

const idx = mesh.thinInstanceAdd(BABYLON.Matrix.Translation(1, 2, 3))  
1

# thinInstanceSetBuffer(name, buffer, stride)

设置实例缓冲区(矩阵 / 自定义属性)

mesh.thinInstanceSetBuffer("matrix", matrixBuffer, 16)
1

# thinInstanceCount

获取 / 设置实例数量

console.log(mesh.thinInstanceCount); // 输出实例总数  
1

# thinInstanceEnablePicking

启用实例拾取(点击交互)

mesh.thinInstanceEnablePicking = true;  
1

# thinInstanceAddSelf()

渲染基础网格自身(作为一个实例)

mesh.thinInstanceAddSelf(); // 基础网格也参与渲染  
1

# 矩阵计算工具

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();
});
1
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  // 返回所有实例的矩阵数组
}
1
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
  1. 模型变换重置
mesh.position.set(0, 0, 0);
mesh.rotation.set(0, 0, 0);
mesh.scaling.set(scale, scale, scale);  
1
2
3
  • 目的:确保源模型处于“基准状态”,避免自身变换影响实例位置
  • 注意:scaling.set(scale)统一缩放模型,避免实例继承错误缩放值
  1. 世界矩阵与逆矩阵计算
const worldMatrix = mesh.getWorldMatrix();
const invWorldMatrix = new Matrix();
worldMatrix.invertToRef(invWorldMatrix);
1
2
3
  • 世界矩阵(World Matrix):包含模型的位置、旋转、缩放信息的 4x4 矩阵
  • 逆矩阵(Inverse Matrix):用于 "抵消" 源模型的变换,使实例位置基于世界坐标系计算
  • 为什么需要:若源模型有初始变换(如导出时不在原点),直接使用平移矩阵会导致实例位置偏移
  1. 实例矩阵计算
// 矩阵乘法顺序:World * (Translation * InverseWorld)
translationMatrix.multiplyToRef(invWorldMatrix, tempMatrix);  // T * invW
const finalMatrix = worldMatrix.multiply(tempMatrix);         // W * (T * invW)
1
2
3
  • 数学意义:

    1. 先用逆矩阵抵消源模型的世界变换(T * invW)
    2. 再用源模型的世界矩阵恢复整体变换(W * ...)
    3. 最终效果:实例位置相对于世界坐标系定位,不受源模型初始变换影响
  • 简化理解:
    相当于把实例 "粘贴" 到世界坐标系的item.position位置,同时保持源模型的缩放和旋转特性