一文读懂 Three.js ShaderMaterial

176 阅读10分钟

在 Three.js 的材质体系中,ShaderMaterial 是连接开发者与 GPU 着色器编程的核心桥梁。允许你完全掌控顶点与片元的渲染流程。与内置的材质(如 MeshBasicMaterial 或 MeshPhongMaterial)不同,ShaderMaterial 不会为你封装任何固定的光照或材质计算逻辑。你需要自己编写顶点着色器(vertex shader)和片元着色器(fragment shader),并通过各种参数将数据传递给 GPU,从而实现自定义渲染。本文将深入剖析其关键属性,帮助开发者掌握自定义着色器的核心控制能力。

一、核心属性详解

ShaderMaterial 继承自 Material 类,通过直接操作 GLSL 着色器代码实现完全可定制的渲染效果。一个简单的 ShaderMaterial 代码如下:

const material = new THREE.ShaderMaterial({
  vertexShader: vertexShaderCode,
  fragmentShader: fragmentShaderCode,
  // 其他配置属性...
});

ShaderMaterial 的核心属性有:

  • vertexShader
  • fragmentShader
  • uniforms
  • defines
  • ...

着色器代码

vertexShader

这是顶点着色器的源码,用于处理每个顶点的变换和属性计算。你可以在这里进行模型的空间变换、计算法线、传递数据到片元着色器等。

// vertexShader.glsl
varying vec2 vUv;
void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
  • 必须包含完整的 GLSL 代码字符串
  • 遵循 WebGL 1.0 规范(Three.js r125+支持 GLSL 3.0)
  • 内置 uniforms 自动注入(如 modelViewMatrix)

fragmentShader

这是片元着色器的源码,负责计算每个片元的最终颜色。你可以在这里实现各种颜色混合、光照计算以及特效处理。

// fragmentShader.glsl
uniform vec3 uColor;
varying vec2 vUv;
void main() {
    gl_FragColor = vec4(uColor * vUv.x, 1.0); // 简单的颜色渐变效果
}

fragmentShader 的 vUv 是上面 vertexShader 插值计算得来的。

并非一个顶点着色器执行完就马上执行一个片元着色器,如后面的图片所示。

顶点着色器是有多少顶点,就运行多少次,而片元着色器则是,生成多少片元,就运行多少次。

uniforms

uniforms 是一个对象,用于传递从 CPU(例如 JavaScript 代码)到 GLSL 着色器中的全局变量。它们通常用于传递诸如变换矩阵(model、view、projection)、时间、光照参数、材质属性等全局数据。

const uniforms = {
  uColor: { value: new THREE.Color(0xff0000) },
  uTime: { value: 0.0 },
};

const material = new THREE.ShaderMaterial({
  vertexShader: vertexShaderSource,
  fragmentShader: fragmentShaderSource,
  uniforms: uniforms,
});

更新机制:

material.uniforms.uTime.value = performance.now() / 1000; // 传递时间
material.needsUpdate = true; // 仅当结构变化时需要

在着色器中,通过 uniform 关键字声明对应的变量,这些变量的值在 GPU 渲染过程中保持不变,直到下次更新。

// vertexShader.glsl
uniform float uTime; // 使用 uniform 变量

attribute vec3 position; // 后面会详细介绍 attribute
attribute vec2 uv;

varying vec2 vUv; // 通过 varying 将 UV 坐标传递到片元着色器

void main() {
  vUv = uv;

  // 利用 uTime 实现简单的顶点位移动画
  vec3 pos = position;
  pos.y += sin(uTime + position.x * 5.0) * 0.1;

  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
// fragmentShader.glsl
uniform vec3 uColor; // 从 JavaScript 传入的颜色值

varying vec2 vUv; // 从顶点着色器传递过来的插值 UV 坐标

void main() {
  // 通过简单的插值和 uniform 混合出最终颜色
  gl_FragColor = vec4(uColor * vUv.x, 1.0);
}

上面的代码中,有两个属性需要重点说下, attribute 和 varyings。

Attributes

  • Attributes 是与几何体的每个顶点相关联的变量,包含顶点的各类数据,如位置、法向量、纹理坐标等。
  • Attributes 只能在顶点着色器中使用,每个顶点拥有各自独立的值。
  • 数据通常存储在缓冲区(Buffer)中,并由 GPU 自动传入到着色器中。

在上面的顶点着色器中,我们使用了两个 attribute:

  • attribute vec3 position:顶点的三维坐标。
  • attribute vec2 uv:顶点的纹理坐标。

如果你需要传入自定义属性,比如每个顶点的颜色,可以这么做:

attribute vec3 position;
attribute vec2 uv;
attribute vec3 aVertexColor; // 自定义属性:顶点颜色

varying vec2 vUv;
varying vec3 vColor; // 定义 varying 将顶点颜色传递给片元着色器

void main() {
  vUv = uv;
  vColor = aVertexColor; // 把属性值赋给 varying

  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
varying vec2 vUv;
varying vec3 vColor; // 接收顶点颜色

void main() {
  // 将传入的顶点颜色与其他效果混合
  gl_FragColor = vec4(vColor * vUv.x, 1.0);
}

在 JavaScript 中,你需要确保在几何体(Geometry 或 BufferGeometry)中添加了 aVertexColor 属性的数据,例如:

const geometry = new THREE.BufferGeometry();
// 假设 positions 和 colors 数组已经存在
geometry.setAttribute(
  'position', 
  new THREE.Float32BufferAttribute(positions, 3)
);
geometry.setAttribute(
  'uv', 
  new THREE.Float32BufferAttribute(uvs, 2)
);
geometry.setAttribute(
  'aVertexColor', 
  new THREE.Float32BufferAttribute(colors, 3)
);

Varyings

Varyings 是用来在顶点着色器和片元着色器之间传递数据的变量。

在顶点着色器中对 varying 赋值后,GPU 会自动对这些值进行插值,然后传递给每个片元。

它们可以用来传递颜色、纹理坐标、法线等信息,使得片元着色器可以获得平滑过渡的数据。

在前面示例中,我们通过 varying 将顶点的 UV 坐标和颜色传递到片元着色器:

attribute vec2 uv;
attribute vec3 aVertexColor;

varying vec2 vUv;
varying vec3 vColor;

void main() {
  vUv = uv;
  vColor = aVertexColor;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
varying vec2 vUv;
varying vec3 vColor;

void main() {
  // GPU 自动插值 vUv 与 vColor,使得片元颜色平滑过渡
  gl_FragColor = vec4(vColor * vUv.x, 1.0);
}

vec4(vColor * vUv.x, 1.0),这部分创建了一个四维向量(vec4),代表 RGBA 颜色。

vColor 是一个从顶点着色器通过 varying 传递下来的 vec3 变量,通常包含了基础颜色信息(例如 RGB 值)。

vUv 也是一个 varying 变量,通常保存每个顶点的纹理坐标,其中 vUv.x 表示水平坐标(通常范围在 0 到 1 之间)。

vColor * vUv.x 这一步将 vColor 中的每个颜色分量都乘以 vUv.x,相当于对颜色进行了一个基于水平纹理坐标的调制。

当 vUv.x 越接近 0 时,乘积会趋向于 0,输出的颜色会更暗;当 vUv.x 越接近 1 时,输出颜色接近原始的 vColor。这种操作常用于实现颜色渐变或其他基于纹理坐标的视觉效果。

1.0 表示 alpha 通道的值,这里设为 1.0 代表完全不透明。

在这个例子中,假设一个三角形的三个顶点有不同的颜色,GPU 会自动根据片元所在位置,线性插值计算出每个片元对应的颜色,这样便产生了平滑的颜色渐变效果。

defines

defines 属性用于在编译着色器时定义宏,这类似于 C/C++ 中的预处理器指令。它可以用于控制着色器代码的条件编译,从而使同一份代码在不同情境下表现出不同的行为。

defines: {
    USE_FOG: true,
    MAX_STEPS: 100
}
#ifdef USE_FOG
    float fogFactor = smoothstep(fogNear, fogFar, vFogDepth);
#endif

其它的一些属性

glslVersion

指定 GLSL 版本,启用现代语法(如 in/out 代替 attribute/varying)。

glslVersion: THREE.GLSL3  // Three.js r125+ 支持

extensions

启用 WebGL 扩展以支持高级功能。

extensions: {
  derivatives: true,    // 启用 `dFdx`/`dFdy`(用于屏幕空间导数)
  fragDepth: true,      // 允许片元着色器修改深度值
  drawBuffers: true     // 支持多渲染目标(MRT)
}

lights

若设为 true,Three.js 会将场景中的光照数据传递到 uniforms 中。

需手动在着色器中处理光照计算(如 phongLighting)。

光照数据通过 uniforms 如 directionalLights、pointLights 传递。

clipping

启用裁剪平面(需配合 renderer.localClippingEnabled = true)。

  • 在顶点着色器中计算 vClipDistance。
  • 在片元着色器中处理 gl_FragCoord 或 gl_ClipDistance。

其他重要属性

  • transparent:是否启用透明度(影响渲染顺序和混合模式)。
  • depthTest:启用深度测试可以确保场景中前后遮挡关系正确,即靠前的物体会遮挡靠后的物体。
  • side:渲染面(THREE.FrontSide/THREE.BackSide/THREE.DoubleSide)。
  • wireframe:是否以线框模式渲染(仅对几何体有效)。
  • wireframeLinewidth:当 wireframe 为 true 时,设置线框的宽度。
  • precision:指定着色器中浮点数的精度(如 "highp"、"mediump" 或 "lowp"),这可能影响性能和视觉质量。

讲到这里,我们看到每个 ShaderMaterial 只关注的是其中一个顶点的数据,那其它的数据是如何存储和调度的?这个就要引出另个概念 BufferGeometry。

二、BufferGeometry

数据存储方式:BufferGeometry 使用 TypedArray(例如 Float32Array)来存储数据,这种方式直接映射到 GPU 缓冲区内存。

BufferGeometry 通过 TypedArray 高效存储数据,适合静态或大规模几何体;旧版 Geometry 以对象形式存储数据,适合动态修改,但性能较低。Three.js 推荐优先使用 BufferGeometry。

BufferGeometry 常见的数据包括:

  • 顶点位置 (position)
  • 顶点法线 (normal)
  • 纹理坐标 (uv)
  • 自定义属性(例如每个顶点的颜色、偏移量等)

索引 (index):除了属性数据外,BufferGeometry 还可以使用索引数组来复用顶点数据,构成面(如三角形),这大大减少了重复数据的存储,提高了渲染效率。

在使用 ShaderMaterial 时,我们会在顶点着色器中声明 attributes,这些变量正是从 BufferGeometry 中取出的数据。整个流程如下:

数据定义与传递

在 JavaScript 中,通过 BufferGeometry.setAttribute() 方法设置每个属性,如 position、uv、aVertexColor 等。

ShaderMaterial 的顶点着色器会使用 attribute 关键字声明对应的变量,这些变量的名字需要与 BufferGeometry 中设置的属性名称保持一致,包括大小写。

通过索引数组可以复用顶点,构建出整个模型的三角形网格,从而高效表示整个几何体。

Uniforms 用于传递整个绘制调用中全局不变的数据(例如视图矩阵、投影矩阵、时间、光照参数等),这些数据在所有顶点和片元中都是一致的。

GPU 并行处理

当场景渲染时,three.js 会将 BufferGeometry 中的数据上传到 GPU。

GPU 会对 BufferGeometry 中的每个顶点并行执行顶点着色器代码,利用这些属性数据进行坐标变换、颜色计算等处理。

虽然每次执行顶点着色器时只处理一个顶点的数据,但 GPU 会同时调度大量的顶点着色器调用。

数据插值

顶点着色器计算后,通过 varying 变量将数据传递到片元着色器。GPU 会对这些 varying 数据自动进行插值,确保整个几何体表面具有平滑的过渡效果。例如三角形的内部进行插值,生成三角形的图元。

下面是一个例子

// 创建 BufferGeometry 实例
const geometry = new THREE.BufferGeometry();

// 定义一个简单三角形的顶点位置数据(3个顶点,每个顶点3个坐标分量)
const positions = new Float32Array([
  0.0, 1.0, 0.0, // 顶点 1
  -1.0, -1.0, 0.0, // 顶点 2
  1.0, -1.0, 0.0, // 顶点 3
]);

// 将位置数据添加为 'position' 属性,3 表示每个顶点由 3 个数值构成
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

// 如果没有预设法线,则调用自动计算法线的方法
geometry.computeVertexNormals();

// 如果需要自定义属性,比如每个顶点的颜色,可以这样设置:
const colors = new Float32Array([
  1.0, 0.0, 0.0, // 红色
  0.0, 1.0, 0.0, // 绿色
  0.0, 0.0, 1.0, // 蓝色
]);
geometry.setAttribute('aVertexColor', new THREE.BufferAttribute(colors, 3));

// 定义顶点着色器
const vertexShaderSource = `
  attribute vec3 position;
  attribute vec3 aVertexColor;  // 自定义属性:顶点颜色
  varying vec3 vColor;          // 将颜色传递给片元着色器

  void main() {
    vColor = aVertexColor;      // 将每个顶点的颜色赋值给 varying
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`;

// 定义片元着色器
const fragmentShaderSource = `
  varying vec3 vColor;

  void main() {
    gl_FragColor = vec4(vColor, 1.0);  // 输出顶点插值后的颜色
  }
`;

// 创建 ShaderMaterial,并传入自定义着色器代码
const material = new THREE.ShaderMaterial({
  vertexShader: vertexShaderSource,
  fragmentShader: fragmentShaderSource,
});

// 使用 BufferGeometry 和 ShaderMaterial 创建一个 Mesh 对象
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

上面的 BufferGeometry 实例如下图所示:

shader.png