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 中的详细赋值尚未展开。想改内部一句话有三种选择:

  1. 在模块前修改它将读取的输入,通常最容易维护。
  2. 在模块后修改它输出的变量,前提是后续阶段还没有消费。
  3. 读取 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 更能表达实现契约。

练习(不附答案)

  1. 在本地源码找到 roughnessFactor 的创建和消费位置,解释适合的修改区间。
  2. 找到 worldPosition 的宏条件,设计一个不能直接使用它的材质配置。
  3. 比较在 skinning 前后施加局部位移的意义,给出一个适合各自位置的效果。
  4. 为“保留颜色贴图纹理的高度染色”写出声明、位置传递和颜色修改的锚点计划。