01 Three.js 怎样把数据交给 Shader

前置知识:能运行最小运行示例。
本章示例:内置变量可视化,完整源码。

学完本章能做什么

你将能分清顶点属性、Three.js 提供的 uniform、自己定义的 uniform,以及由顶点着色器传给片元着色器的数据。看到 position、normal、uv 或一个矩阵时,你应当知道它表示什么、处于哪个坐标空间,并知道它是否需要自己声明。

先看现象

打开本章示例,依次选择“UV”“局部空间法线”“观察空间法线”“局部位置”“世界位置”。同一个球体会出现五种颜色分布。移动模型时,局部位置的颜色仍跟着球面本身;世界位置的颜色会变。移动相机时,观察空间法线的颜色会变。这些颜色都是直接显示 shader 收到或算出的数据,没有使用灯光。UV 模式中球体的一条突变色边是球面 UV 的接缝,并非编译错误。

Three.js 的工作可以先理解为:

BufferGeometry 的顶点数据 ──attribute──> 顶点着色器
模型、相机等场景状态    ──uniform────> 顶点着色器
顶点着色器的输出       ──插值───────> 片元着色器 ──> 颜色

顶点着色器对每个顶点执行,必须写入 gl_Position,决定顶点投到屏幕上的位置。片元着色器对光栅化后的片元执行,写入 gl_FragColor,决定颜色。本教程的入门示例使用 ShaderMaterial;以下“自动提供”均以 Three.js r174 的 ShaderMaterial 为前提。

1. 最常用的三个顶点属性

Three.js 会在顶点着色器源码前加入下面的声明:

attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;
名称 代表什么 典型范围或方向 本章怎样观察
position 顶点在模型局部空间中的位置 由几何体尺寸决定,不限定在 0.0~1.0 选择“局部位置”
normal 顶点在模型局部空间中的表面法线方向 通常是长度为 1 的方向向量 选择“局部空间法线”
uv 几何体为顶点设置的二维纹理坐标 常见网格通常使用 0.0~1.0,但可超出 选择“UV”

声明存在,不表示任意几何体都有有效数据。SphereGeometry 自带这三个属性;自行构造 BufferGeometry 时,要检查相应的 BufferAttribute,例如 geometry.getAttribute( 'uv' )。如果没有 UV,颜色渐变就无法按预期出现。uv 的声明也只位于顶点着色器;片元着色器若要使用它,必须显式传递。

2. 逐一认识自动提供的矩阵

这些矩阵都是 uniform:在一次物体绘制过程中,它们对该物体的所有顶点相同。Three.js 根据物体和相机状态设置它们;使用 ShaderMaterial 时,不要再次声明同名变量。

名称与类型 变换方向 用途
modelMatrix : mat4 局部空间 → 世界空间 把模型顶点放到场景中;移动、旋转、缩放模型会影响它。
viewMatrix : mat4 世界空间 → 观察空间 把世界坐标改写成相机所见的坐标;移动相机会影响它。
modelViewMatrix : mat4 局部空间 → 观察空间 对普通网格,相当于 viewMatrix * modelMatrix,常用于少写一次矩阵乘法。
projectionMatrix : mat4 观察空间 → 裁剪空间 实施透视或正交投影,生成供后续裁剪与透视除法使用的位置。
normalMatrix : mat3 局部法线 → 观察空间法线 根据 modelViewMatrix 的逆转置构造;比直接用模型矩阵乘法线更能正确处理非等比缩放。

最短的常见顶点位置计算是:

gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

矩阵按从右到左作用:先把 position 从局部空间变到观察空间,再投影到裁剪空间。vec4(position, 1.0) 中的 1.0 使平移生效。写成下面这样,在不涉及实例化、蒙皮等额外顶点变换的普通网格上,表示相同的坐标链:

vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
vec4 viewPosition = viewMatrix * worldPosition;
gl_Position = projectionMatrix * viewPosition;

normalMatrix 作用在方向上,而非位置:

vec3 viewNormal = normalize( normalMatrix * normal );

这里得到观察空间法线。顶点位移后,原来的 normal 不会自动变成新表面的法线;第 15、16 章专门处理这个问题。

3. 相机相关的两个值

名称与类型 含义 使用场景
cameraPosition : vec3 相机在世界空间的位置 计算世界空间观察方向,如 cameraPosition - worldPosition。
isOrthographic : bool 当前相机是否为正交相机 需要根据透视与正交投影采用不同算法时使用。

例如,cameraPosition 与世界空间的 worldPosition 可以相减;直接与局部空间的 position 相减则混用了两个坐标系。viewMatrix、cameraPosition、isOrthographic 也会提供给片元着色器;前表中的模型及投影矩阵主要在顶点着色器前缀中声明。

4. 按功能出现的其他属性

下面的声明有条件地加入,并非每个 ShaderMaterial 都有。是否启用取决于几何体、材质设置和渲染功能。

属性 何时出现 代表什么
uv1 : vec2 使用第 2 组 UV 属性时 与基础 uv 独立的第二组纹理坐标。
uv2 : vec2 使用第 3 组 UV 属性时 第三组纹理坐标。
uv3 : vec2 使用第 4 组 UV 属性时 第四组纹理坐标。
color : vec3 或 vec4 启用顶点颜色时 每个顶点的 RGB 或 RGBA 颜色;四分量版本含透明度。
tangent : vec4 使用切线属性时 表面切线及手性信息,常用于法线贴图的坐标变换。
instanceMatrix : mat4 使用实例化渲染时 每个实例自己的变换矩阵。
instanceColor : vec3 实例化颜色启用时 每个实例自己的颜色。
skinIndex : vec4 启用骨骼蒙皮时 影响该顶点的骨骼索引。
skinWeight : vec4 启用骨骼蒙皮时 与骨骼索引对应的权重。
morphTexture : sampler2D 使用实例化形变目标时 保存各实例形变数据的纹理,属于条件提供的 uniform。

例如,实例化网格不能只照搬上面的普通网格位置公式,必须把 instanceMatrix 也纳入变换。形变目标、纹理、光照等功能还有各自的条件定义和数据;本章先掌握最常见的输入,不把它们误认为所有 shader 都自动可用。

5. 把 UV 交给片元着色器

本章示例的顶点着色器有如下代码:

varying vec2 vUv;

void main() {
  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}

片元着色器也要声明同名、同类型的 vUv:

varying vec2 vUv;

void main() {
  gl_FragColor = vec4( vUv.x, vUv.y, 0.25, 1.0 );
  #include <colorspace_fragment>
}

顶点着色器只给每个顶点写一次 vUv;光栅化阶段在三角形内部插值,于是片元着色器获得连续变化的值。vUv 是我们自己定义的名字,Three.js 不会替我们声明或赋值。

ShaderMaterial 与 RawShaderMaterial

ShaderMaterial 会把上文所说的内置声明及其他必要代码加在 shader 源码前。RawShaderMaterial 不做这些自动声明;选用它时,要自己写出将要使用的顶点属性、矩阵等声明。两者都可以接收自己在 JavaScript 中提供的 uniforms。ShaderMaterial 也不会自动替自定义片元着色器计算灯光效果;想要灯光,仍需编写或复用相应的光照计算。

本教程在 r174 中继续使用 attribute、varying、gl_FragColor 等入门写法。Three.js 会为普通 ShaderMaterial 生成 GLSL ES 3.00 程序并加入兼容定义,因此不要把示例字符串的语法形式误认作浏览器最终编译的全部源码。

常见问题

本章小结

顶点属性来自几何体,内置 uniform 来自 Three.js 当前的物体和相机状态;片元着色器需要的数据由顶点着色器通过 varying 传递。下一章会系统地解释这些声明中的 GLSL 数据类型和语法。

本章练习