经纬志
首页 / three.js / 教程

自定义着色器

先看结论

材质参数不够用时,直接写着色器。three.js 已经把相机矩阵、顶点属性准备好了, 只需要接管颜色计算这一段,不必从零搭 WebGL。

最后验证 2026-11-03 · three.js 0.160.0 · 难度:进阶 · 核验 2026-10-29 · 引用本文

两种材质的差别

材质自动注入写法适用
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)。