You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Three.js:能否在BufferGeometry中使用多材质且避免多绘制调用?

在Three.js中用多种材质渲染BufferGeometry且仅单次绘制调用的方案

好问题!先直接给你结论:常规的.addGroup()分组方式确实会触发多次绘制调用,因为每个组对应一次独立的WebGL绘制请求。但如果想要实现「单个BufferGeometry + 多种视觉效果 + 仅一次绘制调用」,可以通过两种进阶方式实现——自定义着色器整合逻辑,或者用纹理图集模拟多材质。

一、自定义着色器(通过onBeforeCompile)

这是最灵活的方案,核心思路是把多种材质的渲染逻辑整合到同一个着色器里,通过顶点属性标记每个顶点/面该使用哪种材质的逻辑,让GPU在一次绘制中完成不同效果的渲染。

实现步骤:

  1. 给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));
  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的大图),然后通过顶点属性传递每个顶点对应的纹理偏移,在着色器中采样不同的纹理区域。

实现步骤:

  1. 准备纹理图集,并给几何体添加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));
  1. 修改着色器,根据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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 09:09:47