Three.js:能否在BufferGeometry中使用多材质且避免多绘制调用?
在Three.js中用多种材质渲染BufferGeometry且仅单次绘制调用的方案
好问题!先直接给你结论:常规的.addGroup()分组方式确实会触发多次绘制调用,因为每个组对应一次独立的WebGL绘制请求。但如果想要实现「单个BufferGeometry + 多种视觉效果 + 仅一次绘制调用」,可以通过两种进阶方式实现——自定义着色器整合逻辑,或者用纹理图集模拟多材质。
一、自定义着色器(通过onBeforeCompile)
这是最灵活的方案,核心思路是把多种材质的渲染逻辑整合到同一个着色器里,通过顶点属性标记每个顶点/面该使用哪种材质的逻辑,让GPU在一次绘制中完成不同效果的渲染。
实现步骤:
- 给BufferGeometry添加自定义属性,标记每个顶点对应的「材质索引」:
// 假设几何体有N个顶点,我们给前半部分顶点分配材质0,后半部分分配材质1 const geometry = new THREE.BoxBufferGeometry(); const materialIndices = new Uint8Array(geometry.attributes.position.count); for (let i = 0; i < materialIndices.length; i++) { materialIndices[i] = i < materialIndices.length / 2 ? 0 : 1; } // 将材质索引作为顶点属性添加到几何体 geometry.setAttribute('materialIndex', new THREE.BufferAttribute(materialIndices, 1));
- 在材质中通过
onBeforeCompile修改着色器,根据材质索引切换渲染逻辑:
const material = new THREE.MeshStandardMaterial({ roughness: 0.5, metalness: 0.3 }); material.onBeforeCompile = (shader) => { // 1. 在顶点着色器中声明自定义属性,并传递给片元着色器 shader.vertexShader = ` attribute float materialIndex; varying float vMaterialIndex; ` + shader.vertexShader; shader.vertexShader = shader.vertexShader.replace( '#include <uv_vertex>', `#include <uv_vertex> vMaterialIndex = materialIndex; // 将材质索引传递到片元着色器 `); // 2. 在片元着色器中根据索引切换材质效果 shader.fragmentShader = ` varying float vMaterialIndex; ` + shader.fragmentShader; shader.fragmentShader = shader.fragmentShader.replace( '#include <color_fragment>', `#include <color_fragment> // 根据材质索引切换颜色/纹理/光照逻辑 if (vMaterialIndex == 0.0) { diffuseColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色材质 } else if (vMaterialIndex == 1.0) { diffuseColor = vec4(0.0, 1.0, 0.0, 1.0); // 绿色材质 } `); };
优缺点:
- ✅ 仅触发一次绘制调用,性能更优
- ✅ 支持完全不同的材质逻辑(比如一个透明、一个金属质感)
- ❌ 需要手写着色器代码,对Three.js着色器体系有一定了解门槛
二、纹理图集 + 顶点UV偏移
如果你的「多种材质」只是纹理不同,那么可以把所有纹理合并成一张纹理图集(比如把2张纹理拼成一张2x1的大图),然后通过顶点属性传递每个顶点对应的纹理偏移,在着色器中采样不同的纹理区域。
实现步骤:
- 准备纹理图集,并给几何体添加UV偏移属性:
const geometry = new THREE.BoxBufferGeometry(); const uvOffsets = new Float32Array(geometry.attributes.position.count * 2); // 假设图集是2x1布局,前半部分顶点用左半纹理,后半部分用右半纹理 for (let i = 0; i < uvOffsets.length; i += 2) { const vertexIndex = i / 2; if (vertexIndex < geometry.attributes.position.count / 2) { uvOffsets[i] = 0.0; // 左半纹理X偏移 uvOffsets[i+1] = 0.0; } else { uvOffsets[i] = 0.5; // 右半纹理X偏移 uvOffsets[i+1] = 0.0; } } geometry.setAttribute('uvOffset', new THREE.BufferAttribute(uvOffsets, 2));
- 修改着色器,根据UV偏移采样图集的不同区域:
const atlasTexture = new THREE.TextureLoader().load('path/to/atlas.png'); const material = new THREE.MeshStandardMaterial({ map: atlasTexture, roughness: 0.5 }); material.onBeforeCompile = (shader) => { shader.vertexShader = ` attribute vec2 uvOffset; varying vec2 vUvOffset; ` + shader.vertexShader; shader.vertexShader = shader.vertexShader.replace( '#include <uv_vertex>', `#include <uv_vertex> vUvOffset = uvOffset; `); shader.fragmentShader = ` varying vec2 vUvOffset; ` + shader.fragmentShader; shader.fragmentShader = shader.fragmentShader.replace( 'vec4 diffuseColor = texture2D( map, vUv );', `// 计算图集UV:原UV缩放+偏移 vec2 atlasUv = vUv * vec2(0.5, 1.0) + vUvOffset; vec4 diffuseColor = texture2D( map, atlasUv ); `); };
优缺点:
- ✅ 仅一次绘制调用,性能优秀
- ✅ 实现相对简单,无需复杂的着色器逻辑
- ❌ 仅适用于材质差异为纹理的场景,无法处理不同的光照/透明属性
为什么.addGroup()会触发多次绘制?
本质是WebGL的底层限制:每次绘制调用只能绑定一套着色器和Uniforms(即一个材质)。.addGroup()的原理是把几何体的索引/顶点分成多个子集,每个子集单独调用一次gl.drawElements/gl.drawArrays,所以必然会触发多次绘制。
如果你的场景中材质差异极大(比如一个是MeshBasicMaterial,一个是MeshPhysicalMaterial),且无法通过着色器整合,那只能接受多次绘制调用——毕竟这是WebGL的原生限制。
内容的提问来源于stack exchange,提问作者duhaime
相关产品推荐
相关产品推荐

