在 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 实例如下图所示: