1. 实例化渲染(InstancedMesh)

前面在第三章几何体合并和性能优化课程里,我们已经简单了解过InstancedMesh优化技术,本课程将对其进行更详细的讲解。

1.1. 基本概念

1.1.1. InstancedMesh vs 普通 Mesh

1.2. 基础使用

1.2.1. 创建 InstancedMesh

javascript

// 创建几何体和材质
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });

// 创建实例化网格(参数:几何体,材质,实例数量)
const count = 1000;
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);

// 创建变换矩阵
const matrix = new THREE.Matrix4();

for (let i = 0; i < count; i++) {
    // 设置位置、旋转、缩放
    const position = new THREE.Vector3(
        (Math.random() - 0.5) * 100,
        (Math.random() - 0.5) * 100,
        (Math.random() - 0.5) * 100
    );

    const rotation = new THREE.Euler(
        Math.random() * Math.PI,
        Math.random() * Math.PI,
        Math.random() * Math.PI
    );

    const scale = new THREE.Vector3(1, 1, 1);

    // 组合变换矩阵
    matrix.compose(position, new THREE.Quaternion().setFromEuler(rotation), scale);

    // 设置实例的矩阵
    instancedMesh.setMatrixAt(i, matrix);
}

scene.add(instancedMesh);

1.2.2. 设置实例颜色(每个实例不同颜色)

javascript

const color = new THREE.Color();

for (let i = 0; i < count; i++) {
    // 设置随机颜色
    color.setHSL(Math.random(), 1.0, 0.5);

    // 设置实例颜色
    instancedMesh.setColorAt(i, color);
}

// 如果设置了颜色,需要将实例颜色启用
instancedMesh.instanceColor.needsUpdate = true;

1.3. 高级用法

1.3.1. 动态更新实例

javascript

// 在动画循环中更新特定实例
function animate() {
    requestAnimationFrame(animate);

    const time = Date.now() * 0.001;
    const matrix = new THREE.Matrix4();

    for (let i = 0; i < count; i++) {
        // 获取当前矩阵
        instancedMesh.getMatrixAt(i, matrix);

        // 分解矩阵获取位置
        const position = new THREE.Vector3();
        const quaternion = new THREE.Quaternion();
        const scale = new THREE.Vector3();
        matrix.decompose(position, quaternion, scale);

        // 更新位置(示例:正弦运动)
        position.y = Math.sin(time + i * 0.1) * 10;

        // 重新组合矩阵
        matrix.compose(position, quaternion, scale);

        // 更新实例
        instancedMesh.setMatrixAt(i, matrix);
    }

    // 标记实例矩阵需要更新
    instancedMesh.instanceMatrix.needsUpdate = true;

    renderer.render(scene, camera);
}

1.3.2. 使用自定义属性

javascript

// 创建自定义着色器材质
const material = new THREE.ShaderMaterial({
    vertexShader: `
        attribute vec3 instanceOffset;
        attribute vec3 instanceColor;
        attribute float instanceScale;

        varying vec3 vColor;

        void main() {
            vColor = instanceColor;

            vec3 pos = position * instanceScale + instanceOffset;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
        }
    `,
    fragmentShader: `
        varying vec3 vColor;

        void main() {
            gl_FragColor = vec4(vColor, 1.0);
        }
    `
});

// 创建 InstancedBufferGeometry
const geometry = new THREE.InstancedBufferGeometry();
geometry.copy(new THREE.BoxGeometry(1, 1, 1));

// 添加实例属性
const offsets = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
const scales = new Float32Array(count);

for (let i = 0; i < count; i++) {
    // 偏移
    offsets[i * 3] = (Math.random() - 0.5) * 100;
    offsets[i * 3 + 1] = (Math.random() - 0.5) * 100;
    offsets[i * 3 + 2] = (Math.random() - 0.5) * 100;

    // 颜色
    colors[i * 3] = Math.random();
    colors[i * 3 + 1] = Math.random();
    colors[i * 3 + 2] = Math.random();

    // 缩放
    scales[i] = Math.random() * 2;
}

geometry.setAttribute('instanceOffset', new THREE.InstancedBufferAttribute(offsets, 3));
geometry.setAttribute('instanceColor', new THREE.InstancedBufferAttribute(colors, 3));
geometry.setAttribute('instanceScale', new THREE.InstancedBufferAttribute(scales, 1));

1.4. 性能优化技巧

1.4.1. 批量更新

javascript

// 避免在每一帧更新所有实例
let needsUpdate = false;

// 只有当实例发生变化时才更新
if (needsUpdate) {
    instancedMesh.instanceMatrix.needsUpdate = true;
    needsUpdate = false;
}

1.4.2. 实例数量管理

javascript

// 动态调整实例数量
function updateInstanceCount(newCount) {
    // 创建新的 InstancedMesh
    const newInstancedMesh = new THREE.InstancedMesh(
        geometry,
        material,
        newCount
    );

    // 复制现有实例数据
    const oldCount = Math.min(newCount, instancedMesh.count);
    const matrix = new THREE.Matrix4();

    for (let i = 0; i < oldCount; i++) {
        instancedMesh.getMatrixAt(i, matrix);
        newInstancedMesh.setMatrixAt(i, matrix);
    }

    // 处理新增的实例
    for (let i = oldCount; i < newCount; i++) {
        // 设置默认矩阵
        matrix.identity();
        newInstancedMesh.setMatrixAt(i, matrix);
    }

    // 替换旧的实例
    scene.remove(instancedMesh);
    instancedMesh = newInstancedMesh;
    scene.add(instancedMesh);
}

1.4.3. 视锥剔除优化

javascript

// InstancedMesh 默认支持视锥剔除
instancedMesh.frustumCulled = true;

1.5. 实际应用示例

1.5.1. 创建大量立方体

javascript

function createInstancedCubes(count) {
    const geometry = new THREE.BoxGeometry(1, 1, 1);
    const material = new THREE.MeshStandardMaterial();

    const instancedMesh = new THREE.InstancedMesh(geometry, material, count);

    const dummy = new THREE.Object3D();

    for (let i = 0; i < count; i++) {
        // 设置变换
        dummy.position.set(
            (Math.random() - 0.5) * 200,
            (Math.random() - 0.5) * 200,
            (Math.random() - 0.5) * 200
        );

        dummy.rotation.set(
            Math.random() * Math.PI,
            Math.random() * Math.PI,
            Math.random() * Math.PI
        );

        dummy.scale.setScalar(Math.random() * 2 + 0.5);

        dummy.updateMatrix();
        instancedMesh.setMatrixAt(i, dummy.matrix);
    }

    return instancedMesh;
}

1.5.2. 交互处理

javascript

// 射线检测实例
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

function onMouseClick(event) {
    // 计算鼠标位置归一化坐标
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

    // 更新射线
    raycaster.setFromCamera(mouse, camera);

    // 检测相交
    const intersects = raycaster.intersectObject(instancedMesh);

    if (intersects.length > 0) {
        const instanceId = intersects[0].instanceId;
        console.log('点击了实例:', instanceId);

        // 高亮选中的实例
        highlightInstance(instanceId);
    }
}

function highlightInstance(instanceId) {
    const color = new THREE.Color(0xff0000);
    instancedMesh.setColorAt(instanceId, color);
    instancedMesh.instanceColor.needsUpdate = true;
}

1.6. 注意事项

  1. 最大实例数限制:取决于硬件和着色器,通常支持数十万实例

  2. 内存管理:及时释放不用的实例

  3. 更新频率:避免每帧更新所有实例

  4. 材质限制:所有实例共享同一个材质

通过合理使用 InstancedMesh,可以在 Three.js 中高效渲染大量重复物体,非常适合制作粒子系统、草地、森林、建筑群等场景。