18 · 补丁组合与升级维护
给材质添加颜色脉冲后再添加溶解,不能简单地两次赋值 onBeforeCompile:后一次会覆盖前一次。即使把两个回调串起来,它们也可能改同一个锚点、声明同名变量,或以相反顺序执行。可维护的补丁系统需要明确所有权和阶段顺序。
最容易忽略的顺序反转
设源码是 #include <color_fragment>。先替换为“include + A”,再对保留的 include 替换为“include + B”,最后得到“include + B + A”。回调调用顺序是 A、B,实际执行顺序却是 B、A。
对同一个锚点的多个追加片段,应先收集,再一次性生成。对于替换整个 Chunk 的补丁,同一位置通常只能有一个所有者;不能默认两个完整替换可以自动合并。
一个范围明确的颜色补丁组合器
下例只负责片元 color_fragment 之后的追加。它不是通用 GLSL 解析器,也不接管未知第三方回调。限制范围让行为容易验证。
import { replaceOnce } from './patch-utils.js';
const demoInstalledMaterials = new WeakSet();
export function installColorPatches(material, patches) {
if (demoInstalledMaterials.has(material)) throw new Error('Patches already installed');
const ids = new Set();
const names = new Set();
const uniforms = {};
const declarations = [];
const bodies = [];
const signatures = [];
for (const patch of patches) {
if (ids.has(patch.id)) throw new Error(`Duplicate patch: ${patch.id}`);
ids.add(patch.id);
signatures.push([patch.id, patch.version, patch.structureKey || '']);
declarations.push(patch.declarations);
bodies.push(patch.body);
for (const name of Object.keys(patch.uniforms)) {
if (names.has(name)) throw new Error(`Duplicate uniform: ${name}`);
names.add(name);
uniforms[name] = patch.uniforms[name];
}
}
const declarationSource = declarations.join('\n');
const bodySource = bodies.join('\n');
const key = JSON.stringify(['demo/color-stack/v1', signatures]);
// 调用者必须将此安装器作为该材质补丁回调的唯一所有者。
material.onBeforeCompile = shader => {
for (const name of Object.keys(uniforms)) {
if (Object.prototype.hasOwnProperty.call(shader.uniforms, name)) {
throw new Error(`Built-in uniform collision: ${name}`);
}
shader.uniforms[name] = uniforms[name];
}
shader.fragmentShader = replaceOnce(
shader.fragmentShader,
'#include <common>',
`#include <common>\n${declarationSource}`,
'stack declarations'
);
shader.fragmentShader = replaceOnce(
shader.fragmentShader,
'#include <color_fragment>',
`#include <color_fragment>\n${bodySource}`,
'stack color'
);
};
material.customProgramCacheKey = () => key;
material.needsUpdate = true;
demoInstalledMaterials.add(material);
return uniforms;
}
每个 patch 形如:
const tintPatch = {
id: 'demo-tint',
version: 1,
structureKey: '',
uniforms: { uDemoTintAmount: { value: 0.3 } },
declarations: 'uniform float uDemoTintAmount;',
body: `diffuseColor.rgb *= mix(vec3(1.0), vec3(1.0, 0.4, 0.2),
clamp(uDemoTintAmount, 0.0, 1.0));`
};
installColorPatches(material, [tintPatch, anotherPatch]) 会按数组顺序执行片段。时间等 uniform 值不进入缓存签名;structureKey 仅记录确实改变代码的选项。声明和代码在安装时被快照固定,后续修改 patch.body 不会悄悄改变已安装逻辑。要变更结构,应创建新配置并重建受控材质,或为系统实现显式重配置 API。
从局部工具扩展为工程模块
真实项目可以设置明确的阶段槽位:顶点位移、法线修正、基础颜色、粗糙度、自发光和最终输出。每个槽位登记唯一锚点及执行顺序;一个补丁可以声明它依赖另一个补丁,但不要默默依赖数组碰巧的排列。
还要记录空间契约。例如一个补丁输出 vDemoLocalPosition,另一个不能将它当世界坐标。法线的输入、输出是否归一化,也应作为模块接口描述。字段名字不同不代表数学语义兼容。
与已有第三方回调组合时,至少保留其调用、this、renderer 参数和原缓存键,还要确认其锚点和源码形态。盲目包装并不能解决语义冲突;未知插件之间应采用显式适配,而不是承诺“任意补丁自动兼容”。
升级 Three.js 时验证什么
- 固定并记录当前版本;对照
ShaderLib主体和目标 Chunk 源码,确认锚点仍存在且唯一。 - 检查变量定义先后、坐标空间与字段名。找到同名 include 不代表内部语义没变。
- 检查 material feature defines 和程序缓存签名。开关组合可能增加此前没有测试的分支。
- 测试有无贴图、顶点颜色、阴影、实例化、非均匀缩放,以及实际使用的 Phong/Standard/Physical 类型。
- 保留能暴露差异的画面,例如移动光源、倾斜面和不同粗糙度,而不是只看纯色正面截图。
r185 的物理材质内部字段例如 diffuseContribution、specularColorBlended 不应照搬其他版本的代码记忆。内部 Chunk 是可查看的实现,并不是所有变量都拥有跨版本稳定承诺。
测试分层
字符串检查发现锚点缺失和重复;源码构建检查发现语法拼接问题;浏览器 WebGL 编译检查发现 GLSL 声明、宏和类型错误;视觉验证才能发现法线、空间、颜色管理及阴影不一致。Vite 构建成功不能代替 GPU 编译和视觉检查。
练习(暂不提供答案)
- 复现“先追加 A 再追加 B,实际 B 在 A 前执行”的现象,并用集中组装修复。
- 为组合器增加一个独立 emissive 槽位,说明为什么不能复用 color 槽位位置。
- 为一个已有插件编写适配清单,列出回调、缓存和 uniform 方面需要确认的事实。
- 为带贴图、阴影和实例化的风吹补丁制定升级验证矩阵,区分静态检查和视觉检查。