04 · 如何查找可靠的替换锚点
1. 从需求反查阶段
“让模型长出波浪”和“让模型看起来有细小水纹”不同:前者需要改变顶点位置及轮廓,后者可能只改变法线和反射。先确认输出数据,才能避免把逻辑塞进最容易找到的字符串。
| 想改变什么 | 优先调查的位置 | 需要确认的问题 |
|---|---|---|
| 局部顶点位置 | begin_vertex 之后、project_vertex 之前 | 是否还需经过 morph、skinning、displacement |
| 基础颜色 | map_fragment / color_fragment 附近 | 替换采样还是保留采样后染色 |
| 透明裁切 | alphatest_fragment 前后或独立 discard | 主材质和阴影是否一致 |
| 粗糙度、金属度 | roughnessmap_fragment / metalnessmap_fragment 之后 | 后续光照材料组装是否已经执行 |
| 最终观察空间法线 | normal_fragment_maps 之后 | 双面、切线空间和 clearcoat 是否需要同步 |
| 自发光 | emissivemap_fragment 之后 | 是增加还是覆盖 totalEmissiveRadiance |
| 光照汇总结果 | opaque_fragment 之前 | 是否仍保留色调映射和输出颜色空间 |
这些是调查起点,不是所有材质通用的保证。例如 Phong 没有 Standard 的 roughnessFactor 流程。
2. 按顺序读模板,而不只搜索关键字
使用编辑器或命令行打开 meshphysical.glsl.js,找到 void main(),沿调用顺序追踪变量。常见的顶点位置链是:
begin_vertex
→ morph target
→ skinning
→ displacement map
→ project_vertex
→ worldpos_vertex / shadowmap_vertex
但法线处理位于 begin_vertex 之前!如果在 begin_vertex 后面才定义一个函数局部波浪变量,再想让前面运行的 normal 代码使用它,程序顺序就不成立。应把公共函数放到全局,或让法线分支基于原始 position 独立计算。
当你找到一个名字时,继续读它的源码:它是否声明变量?是否只在宏条件下声明?输入是哪种空间?读取的是 position 还是 transformed?这些答案决定注入能否运行。
3. 查清楚变量生存范围
例如 begin_vertex 创建 transformed,所以不能在它之前使用这个变量。project_vertex 创建并计算 mvPosition,直接在它之后修改 transformed,不会自动撤销已经写入的 gl_Position。
片元中 roughnessFactor 在粗糙度模块里产生,物理光照材料组装会读取它。若在 lights_physical_fragment 之后才改变 roughnessFactor,材料结构体可能已经保存了此前数值,后续光照不一定受到影响。
worldPosition 还可能受宏条件保护;没有环境贴图、阴影或透射等相关路径时,它未必存在。需要始终可用的自定义世界坐标时,应自行构造独立 varying,并正确处理实例化等前提,不要偷用条件变量。
4. 顶层锚点与嵌套代码
import * as THREE from 'three';
const fragment = THREE.ShaderLib.standard.fragmentShader;
console.log(fragment.includes('#include <lights_physical_fragment>')); // true
console.log(THREE.ShaderChunk.lights_physical_fragment);
模板里只有 include,而 Chunk 中的详细赋值尚未展开。想改内部一句话有三种选择:
- 在模块前修改它将读取的输入,通常最容易维护。
- 在模块后修改它输出的变量,前提是后续阶段还没有消费。
- 读取 Chunk 字符串,在副本上严格替换目标语句,再把副本放回该 include 位置。
第三种与内部具体语句耦合最深,必须记录版本和原始片段,并在升级时检查。不要为了改一句话就手动展开整套着色器,那会扩大维护面。
5. 不要误把声明模块当执行模块
带 pars 的 Chunk 通常包含 uniform、varying、结构体、函数等声明,常位于 main 之外;不带 pars 的模块经常是 main 内的执行代码。但名称只是线索,最终必须看调用位置和源码。
把 diffuseColor.rgb *= ... 插入全局位置,即使 diffuseColor 后面会声明,也不合法。反过来,把一个 GLSL 函数定义塞进 main 内,也不合法。ShaderChunk 不会帮你重新安排作用域。
6. 明确支持范围
第一版补丁可以只支持未蒙皮、未实例化、具有 position/normal/uv 的普通 Mesh,但必须说明。后续扩展到 SkinnedMesh 或 InstancedMesh 时,应重新检查顺序和空间;“在一个球体上成功编译”不是支持所有网格的证据。
每个补丁建议记录:目标材质类型、Three.js 版本、读取与写入变量、支持的几何特性、阴影配套以及缓存结构选项。它们比孤立的一段 replace 更能表达实现契约。
练习(不附答案)
- 在本地源码找到 roughnessFactor 的创建和消费位置,解释适合的修改区间。
- 找到 worldPosition 的宏条件,设计一个不能直接使用它的材质配置。
- 比较在 skinning 前后施加局部位移的意义,给出一个适合各自位置的效果。
- 为“保留颜色贴图纹理的高度染色”写出声明、位置传递和颜色修改的锚点计划。