跳到主要内容
3D Playground
WebGL 与着色器第 9 / 12 节

three.js ShaderMaterial:自定义顶点波浪

进阶WebGL 2

为什么在 three.js 里写着色器

three.js 的内置材质覆盖了大多数需求,但遇到波浪、溶解、全息这类效果时,你需要自己的着色器,同时又想保留 three.js 的场景、相机、控制器和渲染循环。ShaderMaterial 正好是这座桥。

ShaderMaterial:three.js 帮你补齐前缀

使用 ShaderMaterial 时,three.js 会在你的代码前自动插入常用声明:

  • 矩阵:modelMatrix、viewMatrix、projectionMatrix、modelViewMatrix、normalMatrix
  • 相机位置:cameraPosition
  • 几何属性:position、normal、uv

再设置 glslVersion: THREE.GLSL3,three.js 会替你写上 #version 300 es,所以字符串里不要再写版本行,直接用 in / out 即可。在片元着色器结尾调用 linearToOutputTexel(),把线性空间的颜色编码成屏幕所需的 sRGB。

RawShaderMaterial:什么都自己来

RawShaderMaterial 除了版本行之外什么都不加:精度、矩阵 uniform、in vec3 position 都要自己声明。它更啰嗦,但也更透明,适合想精确掌控每一行的人。面板里可以在两种材质之间切换,它们共用同一个 uniforms 对象。

顶点位移

PlaneGeometry(9, 9, 220, 220) 提供大约 5 万个顶点。顶点着色器把每个顶点沿法线方向推高:

glsl
vec3 p = position;
p.z += wave(p.xy);   // 几个正弦波叠加

位移之后原来的法线就不对了。用有限差分估算坡度,重新构造法线:

glsl
float dx = wave(p.xy + vec2(e, 0.0)) - wave(p.xy - vec2(e, 0.0));
float dy = wave(p.xy + vec2(0.0, e)) - wave(p.xy - vec2(0.0, e));
vNormal = normalize(normalMatrix * vec3(-dx, -dy, 2.0 * e));

uniforms:JS 与 GLSL 的接口

uniform 写成 { uTime: { value: 0 } } 的形式。每帧改 uniforms.uTime.value,three.js 就会自动上传,无需手动调用 gl.uniform*。lil-gui 可以直接绑定到 uniforms.uAmplitude 的 value 上,拖动滑块即可实时生效。颜色用 THREE.Color,three.js 会把它转换到线性空间。

调试

给 renderer.debug.onShaderError 设置回调,可以拿到原始的着色器信息日志并用 console.error 打印出来。

核心 API

THREE.ShaderMaterialTHREE.RawShaderMaterialTHREE.GLSL3uniforms.valuePlaneGeometrynormalMatrixlinearToOutputTexelrenderer.debug.onShaderError

动手试试

  1. 1打开 wireframe,看看位移后的网格。
  2. 2把 PlaneGeometry 的分段数改成 20,波浪会变成什么样?
  3. 3在 wave() 里加入以鼠标位置为中心的圆形涟漪(新增一个 uMouse uniform)。
运行中