17 · 克隆、共享与多对象状态
场景里有十台设备,各自有不同的报警进度。问题不是简单地“复制 shader”,而是决定哪些东西共享:纹理、GPU 程序、材质对象、uniform 容器和业务状态是不同层次。
clone 不会替你复制补丁
r185 的 Material.clone() 调用 new this.constructor().copy(this)。copy() 复制材质已有的属性,但不复制实例上的 onBeforeCompile 和 customProgramCacheKey 回调。onBeforeCompile 注入的 uniforms 也不是 MeshStandardMaterial 公共属性,因此不会作为自定义状态自动复制。
const b = a.clone() 可以得到相同粗糙度和基础色,却没有原来的补丁效果。材质 JSON 序列化也不能携带可执行的补丁函数。userData 适合保存可序列化配置;r185 的 copy 使用 JSON 方式复制 userData,不宜把函数、纹理实例或循环引用状态藏在其中并期待保留语义。
用工厂重新创建,而不是复制编译实例
第 13 章的工厂已经返回稳定 uniforms。为业务增加一个显式克隆函数:
// createPulseMaterial 为第 13 章的材质工厂。
function clonePulse(source) {
const target = createPulseMaterial();
// copy 不会把 source 的回调带过来,也不应承担补丁状态复制。
// target 已由工厂安装好自己的回调和缓存键。
target.material.copy(source.material);
target.uniforms.uDemoTime.value = source.uniforms.uDemoTime.value;
target.uniforms.uDemoPulseStrength.value = source.uniforms.uDemoPulseStrength.value;
target.uniforms.uDemoPulseColor.value.copy(source.uniforms.uDemoPulseColor.value);
return target;
}
const first = createPulseMaterial();
const second = clonePulse(first);
second.uniforms.uDemoPulseColor.value.set('#4a88c7');
这个例子针对工厂产生的同类 MeshStandardMaterial,且 copy 在首次渲染之前进行。不要对已经安装了未知第三方补丁的任意材质套用它。更完整的业务工厂可接受纯配置,直接根据配置建立新材质和状态,便于保存和加载。
若是 Vector、Color、Matrix,复制值通常用 .copy();若直接赋相同对象,就共享了可变状态。纹理通常可以共享:复制材质贴图引用不会复制像素资源,这往往是期望的行为。
有意共享时间,独立保存颜色
很多对象应当同步使用同一时钟,却有各自的参数。让工厂接受显式的时钟容器:
const sharedTime = { value: 0 };
// 在工厂创建 uniforms 时采用:
const uniforms = {
uDemoTime: sharedTime,
uDemoPulseStrength: { value: 0.5 },
uDemoPulseColor: { value: new THREE.Color('#e8795f') }
};
此片段展示的是工厂内部替代写法,需要按第 13 章安装对应声明与计算。每帧只更新 sharedTime.value;不同工厂实例的颜色容器仍独立。创建关系时就共享容器,不要等编译后再把业务字典的某个字段换掉,旧 shader.uniforms 仍可能指向原容器。
一个材质并不等于每个对象有独立 uniform
meshA.material = material;
meshB.material = material;
此时它们使用同一个材质状态。把颜色 uniform 更新为蓝色,预期就是两个对象都变蓝。如果要每个对象独立颜色,最直接的选择是多个材质实例、相同缓存键、各自 uniform 容器。它们仍可共享兼容 GPU 程序,不必把“多个材质”误解为“必须重复编译相同代码”。
常见建议是在 mesh.onBeforeRender 中改共享材质 uniform。对普通内置材质,这不是无条件可靠的逐对象上传方案。r185 WebGLRenderer 的材质 uniforms 上传发生在 refreshMaterial 等控制下;连续绘制同一材质对象时,修改 JavaScript 容器不保证触发额外上传。
uniformsNeedUpdate 的强制上传分支检查 material.isShaderMaterial。给 MeshStandardMaterial 临时增加同名字段不能把它变成 ShaderMaterial;伪造 isShaderMaterial 更会破坏材质处理流程。needsUpdate 是程序结构更新通知,也不是恰当的逐对象数据上传 API。
如果对象很多,可按需求选择:
| 需求 | 更合适的组织方式 |
|---|---|
| 少量对象,各自参数 | 独立材质实例,共享代码签名和纹理 |
| 大量重复几何,各自参数 | InstancedMesh + 实例 attribute |
| 每顶点变化 | BufferAttribute,经 varying 传至片元 |
| 大量索引化数据 | 数据纹理及明确索引,评估采样开销 |
| 特殊逐 draw 上传控制 | 选择有明确上传契约的自定义渲染方案,避免依赖未承诺的内置刷新时机 |
实例属性在同一次 draw 中可以不同,而 uniform 在一次 draw 中是共享的。这是数据模型的区别,不只是 Three.js API 的限制。
配套材质与生命周期
顶点变形的主材质、customDepthMaterial 和 customDistanceMaterial 应引用同一个变形时间和幅度容器,见第 24 章。每个对象独立的变形参数也必须在其阴影配套材质中独立,否则主画面和阴影会错位。
业务控制器应持有这些对象并统一释放材质。共享纹理和共享时钟由拥有它们的上层管理。复制对象时明确复制的是配置、值还是资源引用,不要只依赖递归深拷贝。
练习(暂不提供答案)
- 对带补丁材质执行 clone,检查回调身份和最终效果,再用工厂恢复功能。
- 创建三台设备,共享时间、独立颜色和强度,证明修改其中一个颜色不会影响另外两个。
- 对比共享材质与独立材质的状态关系,并解释为什么共享 GPU 程序仍允许独立 uniform。
- 为风吹模型设计主材质和阴影材质的克隆接口,列出必须共享与必须独立的数据。