06 · 全局声明、UV 与顶点颜色

返回系列目录

本章以 Three.js r185 的 meshphong.glsl.js、meshphysical.glsl.js 和相应 ShaderChunk 为依据。ShaderChunk 不是可以任意排列的插件:声明必须早于使用,顶点输出必须与片元输入一致,宏必须与几何数据及材质配置相符。本章代码放在 material.onBeforeCompile = (shader) => { ... } 内,除非另有说明;示例不依赖材质已经开启 map。

1. common:公共函数与结构的声明区

它位于两个着色器的 main() 外,定义 PI、saturate、若干幂函数、方向变换辅助函数,以及 IncidentLight、ReflectedLight 等结构。它没有一个统一的输入输出变量,作用是为后续代码提供类型和函数。不要整体移除,否则缺少的声明可能直到光照模块才暴露。

这里适合追加自己的 uniform、varying、函数。例如在顶点和片元源码中分别追加 varying vec2 vDemoUv;。这些是全局声明,不是每个顶点运行一次的赋值。函数也应放在这里,GLSL 不允许在 main() 内定义另一个函数。

shader.vertexShader = shader.vertexShader.replace(
  '#include <common>',
  '#include <common>\nuniform float uDemoTime;\nvarying vec2 vDemoUv;'
);
shader.fragmentShader = shader.fragmentShader.replace(
  '#include <common>',
  '#include <common>\nvarying vec2 vDemoUv;'
);

避免使用 normal、material、PI、saturate 等内置名字。函数也加前缀,例如 demoStripeMask()。r185 的 inverseTransformDirection 已是兼容别名;复制旧教程时优先核对当前函数名与空间假设,尤其不要把视图矩阵专用的正交变换技巧用于任意带缩放的矩阵。

2. uv_pars_vertex:声明不同贴图的 UV 通道

这个顶点声明模块由 USE_MAP、USE_NORMALMAP 等宏控制,分别声明 mapTransform、normalMapTransform 等 mat3 和 vMapUv、vNormalMapUv 等 varying。这里还可能声明 vUv,但它受 USE_UV 或 USE_ANISOTROPY 控制,不能假设任意 Standard 材质里都存在 vUv。

纹理坐标不是世界坐标。UV 的单位是纹理域,一个单位通常对应一次纹理宽度或高度;它也可以超出 0~1,最终如何采样由 wrap 设置决定。mapTransform 表达平移、缩放和旋转,因此颜色贴图与法线贴图可以有不同的采样变换。

新增自定义贴图时,可以声明自己的 uniform sampler2D uDemoPattern; 和 varying vec2 vDemoUv;,无需伪造 USE_MAP。手动打开内置宏会连带要求内置 sampler、UV 宏和数据全部有效。

3. uv_vertex:计算待插值的 UV

它在顶点 main() 内读取几何 UV attribute,输出前述 varying。以颜色贴图为例:

vMapUv = ( mapTransform * vec3( MAP_UV, 1 ) ).xy;

MAP_UV 由当前贴图通道配置决定,可能不是 uv。这里的齐次分量 1 允许 Matrix3 执行二维平移。若变换矩阵为 T、UV 为 q,实际采样位置是 T * vec3(q, 1)。修改采样坐标向右并不等于可见图案向右:固定屏幕点采到了右侧纹素,图案通常表现为向左流动。

要使自定义图案不依赖内置贴图设置,可以在保留该模块之后执行:

shader.vertexShader = shader.vertexShader.replace(
  '#include <uv_vertex>',
  `#include <uv_vertex>
   vDemoUv = uv + vec2(uDemoTime * 0.1, 0.0);`
);

前提是几何体含 uv attribute;PlaneGeometry、BoxGeometry 等通常具有,但自建 BufferGeometry 必须明确设置。改变 UV 不会移动网格,也不会改变阴影轮廓。想同步颜色、粗糙度和法线的流动,就需要统一它们的坐标策略,而不是只更新 vMapUv。

4. uv_pars_fragment:接收插值后的坐标

它在片元全局区声明与顶点端对应的 UV varying。光栅器根据三角形顶点输出执行透视正确插值,一个片元通常得到两端之间的值。顶点计算 sin(uv.x) 再插值,与片元对插值后的 uv.x 求 sin 不相同:非线性函数一般不能与插值交换顺序。

因此用于条纹阈值、细噪声和精细采样的非线性计算通常放在片元端。顶点端只传 UV,可减少对网格细分程度的依赖。自定义 varying 要在两端使用完全一致的类型和名称;片元声明 vec3、顶点声明 vec2 会导致链接错误。

5. color_pars_vertex:顶点颜色声明

r185 在相关宏开启时声明 varying vec4 vColor,包括普通顶点颜色、带 alpha 的颜色、实例颜色和 batching 颜色。不要沿用旧版本“vColor 一定是 vec3”的假设。

输入 color attribute 一般承载线性工作空间中的颜色数据。给每个顶点存颜色能表达烘焙渐变、权重和分区,但数据的业务语义应明确:如果想存独立的风力权重,独立 attribute 往往比借用颜色通道更容易维护。

6. color_vertex:合成顶点、实例与批次颜色

此模块先把 vColor 设为全 1,再依据宏乘入 attribute 的 RGB/RGBA、instanceColor、batching 颜色。结果仍是顶点数据,尚未与材质 diffuse 相乘。白色是乘法单位元,因此不会改变材质原色。

在它之前给 vColor 赋值,会被初始化覆盖;在它之后修改才会保留。如果启用了 morph color,后面还有 morphcolor_vertex,最终结果又可能变化。操作这类中间值前必须阅读 ShaderLib 实际执行顺序。

7. color_pars_fragment:片元颜色输入

它在 USE_COLOR 或 USE_COLOR_ALPHA 下声明 varying vec4 vColor。这里的宏条件与顶点模块文本不完全相同,具体程序参数由 renderer 配合生成。不要仅复制其中一个模块就认为已经完整支持 InstancedMesh。

如果自己创建不同含义的颜色 varying,应声明 vDemoTint,不要抢占 vColor。声明不会自动使几何体增加 attribute,也不会自动分配数据。

8. color_fragment:把颜色乘入材质基础色

在片元 main() 中,它执行 diffuseColor *= vColor。其中 diffuseColor 已由材质 diffuse/opacity 初始化,并且已通过 map_fragment。此时仍处在光照之前,所以改变 RGB 会影响后续漫反射和 Standard 的金属反射颜色,而不是无视灯光地覆盖屏幕结果。

以设备表面的流动条纹为例,可在该模块之后混合颜色:

shader.fragmentShader = shader.fragmentShader.replace(
  '#include <color_fragment>',
  `#include <color_fragment>
   float demoStripe = 0.5 + 0.5 * sin(vDemoUv.x * 40.0);
   diffuseColor.rgb = mix(diffuseColor.rgb, vec3(0.1, 0.5, 0.8), demoStripe * 0.4);`
);

此例中的 GLSL 常量是线性工作空间数值,不是把 CSS 十六进制颜色直接除以 255 后得到的 sRGB 值。完整安装时还需在 JavaScript 建立并注入 shader.uniforms.uDemoTime = timeUniform,动画循环更新 timeUniform.value。源代码不随时间变化,时间不需要进入程序缓存键。

9. 顶层锚点与嵌套模块

本章八个名称都可在对应 Phong/Physical ShaderLib 主体中找到。但“一个函数来自某 Chunk”不等于函数文本已经出现在 shader.fragmentShader:回调中通常仍是 include。直接 replace('diffuseColor *= vColor;', ...) 不会命中尚未展开的 color_fragment。

保留 include 后追加代码最稳妥。如果确需修改内部,先读取 THREE.ShaderChunk.color_fragment,局部修改它的副本,再用副本替换本材质的 include。不要修改全局 THREE.ShaderChunk,否则所有后续使用它的材质都可能受影响。生产代码应像第 05 章一样检查锚点存在且唯一。

练习(暂不提供答案)

  1. 给没有 map 的 Standard 材质加入自定义 UV 棋盘格,解释为何不能依赖 vMapUv。
  2. 将条纹的 sin 从片元移到顶点,在只有两个三角形和平面高度细分时分别观察差异。
  3. 给网格设置顶点颜色,预测 map、material.color 与 vertexColors 三者相乘的结果,并用纯红和纯绿验证。
  4. 让自定义条纹使用 Matrix3 变换,同时保持原来的 map 不变;列出需要声明和更新的数据。