如何在 Three.js 中使用 Shader
这篇教程将带你了解在 Three.js 中使用自定义 Shader(着色器)的基础流程。
1. 什么是 Shader
- Shader(着色器) 是运行在 GPU 上的一个小程序,用来对图形的顶点和像素进行自定义的处理。
- 在 WebGL(以及 Three.js)中,常见的着色器分为两种:
- 顶点着色器(Vertex Shader):负责处理每个顶点的坐标变换、计算法线等顶点相关的信息。
- 片元着色器(Fragment Shader,或称像素着色器):负责确定每个像素最终的颜色、透明度等效果。
在 Three.js 中,如果想要完全掌控物体外观和渲染细节,就需要使用 ShaderMaterial
并编写自己的顶点着色器和片元着色器。
2. Three.js 中使用 Shader 的基本流程
-
编写顶点着色器(vertexShader)和片元着色器(fragmentShader)
- 使用 GLSL 语言编写,存放在字符串中或单独的文件里。
- 在顶点着色器中,你需要至少计算出顶点在屏幕上的位置(
gl_Position
)。 - 在片元着色器中,你需要至少为每个像素设置一个颜色(
gl_FragColor
)。
-
创建
ShaderMaterial
并将着色器传入const material = new THREE.ShaderMaterial({ vertexShader: myVertexShader, fragmentShader: myFragmentShader, uniforms: { // 需要传入着色器的参数可以在这里定义 }, });
-
将
ShaderMaterial
应用于几何体(Geometry/Mesh)并将其添加到场景const geometry = new THREE.BoxGeometry(1, 1, 1); // 例如立方体 const cube = new THREE.Mesh(geometry, material); scene.add(cube);
-
在渲染循环中更新 uniforms 或其他状态(如果需要动态效果)
function animate() { requestAnimationFrame(animate); // ... 更新逻辑 ... renderer.render(scene, camera); } animate();
3. 一个简单的示例:渐变颜色
下面我们来实现一个简单的示例:使用自定义 Shader 让一个平面从左到右呈现渐变色。
3.1 基本场景搭建
首先,准备好一个最基本的 Three.js 场景,一般的步骤包括:
- 新建场景(Scene)。
- 新建透视摄像机(PerspectiveCamera)。
- 新建渲染器(WebGLRenderer)。
- 将渲染器挂载到页面中。
- 设置渲染循环。
示例代码(可省略一些基础配置):
// 1. 新建场景
const scene = new THREE.Scene();
// 2. 新建摄像机
const camera = new THREE.PerspectiveCamera(
75, // 视角角度
window.innerWidth / window.innerHeight, // 纵横比
0.1, // 最近面
1000 // 最远面
);
camera.position.z = 5; // 拉远摄像机
// 3. 新建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 4. 监听窗口大小变化(可选)
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 5. 渲染循环
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
3.2 编写顶点着色器
我们的顶点着色器需要完成的工作:
- 接收每个顶点的坐标(
position
)。 - 对坐标进行 MVP(模型-视图-投影)变换。
- 将最终结果赋值给内置变量
gl_Position
。 - 另外可以计算和传递一些自定义信息给片元着色器,这个示例中我们传递 x 坐标用于计算渐变。
示例顶点着色器(GLSL):
// myVertexShader.glsl
varying float vXPos;
void main() {
// Three.js 自动提供了 position 变量
// 这里将其乘以模型视图投影矩阵得到裁剪空间位置
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
// 传递顶点的 x 坐标给片元着色器
vXPos = position.x;
}
说明:
varying float vXPos;
是一个顶点 -> 片元的可变变量,用于传递数值到片元着色器。position
,projectionMatrix
,modelViewMatrix
等是 Three.js 默认传入的内置变量/矩阵。
3.3 编写片元着色器
我们的片元着色器需要做的工作:
- 接收顶点着色器传递来的
vXPos
。 - 根据
vXPos
计算颜色。 - 将颜色赋值给
gl_FragColor
。
示例片元着色器(GLSL):
// myFragmentShader.glsl
// 从顶点着色器接收变量
varying float vXPos;
void main() {
// 将 x 坐标映射到 [0,1] 范围的示例,假设 x 坐标区间 [-1,1]
// 为简单起见,这里假设平面 Geometry 中 x 的范围是 [-1,1]
float t = (vXPos + 1.0) * 0.5;
// 通过 t 在两种颜色之间做线性插值
// 比如由蓝色(0, 0, 1)过渡到红色(1, 0, 0)
vec3 color = mix(vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0), t);
// 赋值给 gl_FragColor
gl_FragColor = vec4(color, 1.0);
}
在真实项目中,如果 x 坐标范围并非 [-1,1],就需要根据你的几何体实际范围来进行归一化。
3.4 使用 ShaderMaterial
现在我们就可以将着色器字符串或文件内容传给 ShaderMaterial
并创建一个网格。
// 将我们写好的着色器源码以字符串的形式定义或引入
const myVertexShader = `
varying float vXPos;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
vXPos = position.x;
}
`;
const myFragmentShader = `
varying float vXPos;
void main() {
float t = (vXPos + 1.0) * 0.5;
vec3 color = mix(vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0), t);
gl_FragColor = vec4(color, 1.0);
}
`;
// 创建 ShaderMaterial
const material = new THREE.ShaderMaterial({
vertexShader: myVertexShader,
fragmentShader: myFragmentShader,
});
// 创建一个平面几何体
const geometry = new THREE.PlaneGeometry(2, 2); // x 范围大约在 [-1, 1]
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);
shader 引用
大部分场景下,我们会创建 vertext.glsl
和 fragment.glsl
用来放置 glsl 的代码,然后在 js 文件中进行 import 使用
import vertexShader from "./shaders/vertex.glsl";
import fragmentShader from "./shaders/fragment.glsl";
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
});
在这个示例中,你会看到平面左侧呈蓝色,向右渐变到红色。如果你想要让渐变方向竖直显示,可以用 position.y
来做插值。
4. 传递 Uniforms
在稍微复杂一点的效果里,我们经常需要在 JS 代码中实时更新一些参数给着色器,这时就会用到 uniforms。它可以理解为着色器中不可变(相对于 varying 和 attribute 而言)的全局变量,但可以通过 JS 修改它的值(渲染时生效)。
例子:我们让时间不断变化,从而让颜色随时间波动。
-
在 material 中定义一个
uniforms
:const material = new THREE.ShaderMaterial({ vertexShader: myVertexShader, fragmentShader: myFragmentShader, uniforms: { uTime: { value: 0.0 }, }, });
-
在着色器中声明 uniform,并使用它:
// 顶点或片元着色器 uniform float uTime;
-
在 JS 的渲染循环中修改:
let startTime = Date.now(); function animate() { requestAnimationFrame(animate); let elapsed = (Date.now() - startTime) * 0.001; // 转换成秒 material.uniforms.uTime.value = elapsed; renderer.render(scene, camera); } animate();
-
在着色器(通常在片元着色器)中结合
uTime
做动画:// 片元着色器示例 uniform float uTime; varying float vXPos; void main() { float t = (vXPos + 1.0) * 0.5 + sin(uTime) * 0.25; vec3 color = mix(vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0), t); gl_FragColor = vec4(color, 1.0); }
这样,颜色就会随着 uTime
的变化而产生动态波动。
6. 总结 & 提示
- ShaderMaterial 对于需要高度可定制化的场景非常有用,但如果只是想简单调节颜色、纹理等,Three.js 本身提供了诸如
MeshBasicMaterial
,MeshStandardMaterial
等现成材质,可以先了解和使用它们。 - uniforms 可以传递多种数据类型给着色器:
float
,vec2
,vec3
,vec4
,sampler2D
(纹理)等。 - varying 在顶点着色器中声明并赋值,用于传递给片元着色器做插值。
- attributes(顶点属性)在 Three.js 中通常由几何体的数据自动管理,常见的如
position
,normal
,uv
等。
代码
github
https://github.com/calmound/threejs-demo/tree/main/shader (opens in a new tab)
gitee
https://gitee.com/calmound/threejs-demo/tree/main/shader (opens in a new tab)