自定义着色器
材质参数不够用时,直接写着色器。three.js 已经把相机矩阵、顶点属性准备好了, 只需要接管颜色计算这一段,不必从零搭 WebGL。
两种材质的差别
| 材质 | 自动注入 | 写法 | 适用 |
|---|---|---|---|
ShaderMaterial | 相机矩阵、顶点属性、精度声明 | 直接使用 position、normal | 绝大多数自定义效果 |
RawShaderMaterial | 无 | 自己声明全部变量 | 移植现成 GLSL、需要完全控制 |
两者都不参与 three.js 的默认光照与阴影。需要受光时,要么自己在着色器里算,
要么使用 onBeforeCompile 改写内置材质。
按高程着色
顶点着色器把高程传给片元,片元按高程在两种颜色之间插值。
normalMatrix 与 modelViewMatrix 是内置的,
不需要在 uniforms 里重复声明。
const vertexShader = `
varying vec3 vNormal;
varying float vHeight;
void main() {
vNormal = normalize(normalMatrix * normal);
vHeight = position.y;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform vec3 lowColor;
uniform vec3 highColor;
varying vec3 vNormal;
varying float vHeight;
void main() {
float t = clamp((vHeight - 20.0) / 60.0, 0.0, 1.0);
vec3 base = mix(lowColor, highColor, t);
// 简易方向光,方向固定为上方
float light = 0.45 + 0.55 * max(dot(vNormal, vec3(0.0, 1.0, 0.0)), 0.0);
gl_FragColor = vec4(base * light, 1.0);
}
`;
const terrainMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms: {
lowColor: { value: new THREE.Color('#1f5fd0') },
highColor: { value: new THREE.Color('#d0453a') }
},
side: THREE.DoubleSide
});
varying 变量在顶点与片元两侧的类型必须完全一致,
一边写 float、一边写 vec3 会直接编译失败,
错误只出现在控制台的着色器日志里。
用时间驱动动画
着色器里的动画通过 uniform 传入时间。每帧只更新一个浮点数, 顶点数据完全不动,比在 JS 里改几何便宜得多。
const shaderMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uScale: { value: 0.15 }
},
vertexShader: `
uniform float uTime;
uniform float uScale;
varying float vWave;
void main() {
vWave = sin(position.x * uScale + uTime) * 0.5 + 0.5;
vec3 displaced = position;
displaced.y += vWave * 2.0;
gl_Position = projectionMatrix * modelViewMatrix * vec4(displaced, 1.0);
}
`,
fragmentShader: `
varying float vWave;
void main() {
gl_FragColor = vec4(0.12, 0.40, 0.85, 0.6 + vWave * 0.4);
}
`,
transparent: true,
depthWrite: false
});
const clock = new THREE.Clock();
renderer.setAnimationLoop(function () {
shaderMaterial.uniforms.uTime.value = clock.getElapsedTime();
renderer.render(scene, camera);
});
transparent: true 时通常配合 depthWrite: false,
避免半透明面互相遮挡产生错误的深度写入。代价是这些面不再参与排序之外的遮挡。
颜色值按线性空间给。THREE.Color 从十六进制构造时会做 sRGB 转换,
直接把 0x1f5fd0 写进着色器则会偏亮。差异不大但会影响整幅画面的观感。
注意事项
- 着色器编译失败不会抛出异常,只在控制台打印日志,开发时要保持控制台可见。
uniforms对象要一直持有引用,每帧重新赋值时改的是.value,不是替换整个对象。- 移动端对浮点精度敏感,片元着色器里尽量用
mediump,需要精度时再声明highp。 - 修改着色器源码字符串后必须新建材质,运行中改字符串不会触发重新编译。
- 自定义材质不参与阴影投射,需要阴影时改用内置材质的
onBeforeCompile。
经纬志(2026)。《自定义着色器》。https://www.jingweizhi.com/three/tutorial/custom-shader.html(访问日期 2026-11-03)。