如何合并同网格不同颜色对象并实现单次Draw Call?
嘿,这个问题我刚好踩过坑!你之前的思路没抓住核心:要实现单次Draw Call,必须让所有渲染数据都整合到「单个几何+单个材质」里,而不是用多个材质数组——这也是为什么你之前的方法Draw Call数量和材质数一致,因为Three.js会自动把多材质的Mesh拆分成多个子渲染单元,自然触发多次调用。
下面给你两种可行的解决方案,分别适用于不同场景:
方案一:合并几何并嵌入顶点颜色(适合中小数量Mesh)
这种方法的核心是把每个Mesh的颜色信息,直接写入合并后几何的顶点属性中,用支持顶点颜色的单材质渲染,就能实现单次Draw Call。
实现步骤&代码示例
// 假设你的所有Mesh都基于同一个基础几何baseGeo,存在meshes数组中 const mergedGeo = new THREE.BufferGeometry(); const positions = []; const vertexColors = []; // 遍历所有Mesh,收集顶点位置和对应颜色 meshes.forEach(mesh => { const targetColor = mesh.material.color; const posArray = baseGeo.attributes.position.array; // 给每个顶点复制位置,并绑定当前Mesh的颜色 for (let i = 0; i < posArray.length; i += 3) { positions.push(posArray[i], posArray[i+1], posArray[i+2]); vertexColors.push(targetColor.r, targetColor.g, targetColor.b); } }); // 设置合并后几何的属性 mergedGeo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); mergedGeo.setAttribute('color', new THREE.Float32BufferAttribute(vertexColors, 3)); mergedGeo.computeVertexNormals(); // 如果需要光照效果,记得计算法线 // 关键:使用支持顶点颜色的材质,开启vertexColors const mergedMaterial = new THREE.MeshStandardMaterial({ vertexColors: true, // 必须开启这个属性才能读取顶点颜色 // 这里可以保留原材质的其他属性,比如metalness、roughness等 }); // 创建最终的合并Mesh const mergedMesh = new THREE.Mesh(mergedGeo, mergedMaterial); scene.add(mergedMesh);
方案二:使用InstancedMesh(适合大量相同Mesh,性能更优)
如果你的Mesh数量很多(比如几百上千个),合并顶点会导致几何数据量暴增,这时用InstancedMesh是更好的选择——它共享同一个几何和材质,通过实例化矩阵和实例化颜色来实现不同外观,全程只触发一次Draw Call,性能开销极小。
实现步骤&代码示例
// 基于第一个Mesh的几何和材质创建基础资源 const baseGeo = meshes[0].geometry; const baseMaterial = new THREE.MeshStandardMaterial({ instancedColors: true, // 关键:开启实例化颜色支持 // 保留原材质的其他属性 }); // 创建InstancedMesh,参数:几何、材质、实例总数 const instancedMesh = new THREE.InstancedMesh(baseGeo, baseMaterial, meshes.length); const tempMatrix = new THREE.Matrix4(); const tempColor = new THREE.Color(); // 遍历所有Mesh,设置每个实例的矩阵(位置/旋转/缩放)和颜色 meshes.forEach((mesh, index) => { // 复制当前Mesh的变换矩阵 mesh.updateMatrix(); tempMatrix.copy(mesh.matrix); instancedMesh.setMatrixAt(index, tempMatrix); // 设置当前实例的颜色 tempColor.copy(mesh.material.color); instancedMesh.setColorAt(index, tempColor); }); // 标记实例数据需要更新 instancedMesh.instanceMatrix.needsUpdate = true; instancedMesh.instanceColor.needsUpdate = true; scene.add(instancedMesh);
为什么你的原方法不行?
当你给单个Mesh传入材质数组时,Three.js会自动将几何按材质对应的面拆分,生成多个隐性的子Mesh,每个子Mesh对应一个材质,自然会触发和材质数量相同的Draw Call——这是Three.js的渲染机制决定的,所以这种思路从一开始就不符合单次Draw Call的要求。
注意事项
- 不管用哪种方案,材质必须支持顶点颜色或实例化颜色:比如
MeshStandardMaterial、MeshBasicMaterial都支持,但部分特殊材质(比如MeshDepthMaterial)可能不支持,需要提前确认。 - 如果原材质带有纹理,只要所有Mesh的纹理相同,直接复用即可,不影响两种方案的实现。
InstancedMesh除了颜色,还可以单独控制每个实例的位置、旋转、缩放,灵活性非常高,是处理大量相同Mesh的首选方案。
内容的提问来源于stack exchange,提问作者user9731024




