34 · 构建可复用的定制材质模块
1. 工厂返回完整效果,而不只是一个 Material
一个可靠效果通常包括主材质、uniform 状态、结构配置、阴影配套、安装方式和资源释放。把它们散落在组件事件、onBeforeCompile 闭包和全局 shader 变量中,会让克隆和卸载变得困难。
本章提供完整源码 create-dissolve-bundle.js。它创建一组具有相同裁切遮罩的 Standard、Depth、Distance 材质,并演示可复用的所有权设计。它与第 30 章的重点不同:这里关注模块边界和生命周期,而不是效果公式本身。
返回接口如下:
| 字段 / 方法 | 职责 |
|---|---|
material |
可见表面,保留 Standard 光照并增加边缘 emissive |
depthMaterial |
方向光、聚光灯等对应的自定义深度路径 |
distanceMaterial |
点光源对应的自定义距离路径 |
uniforms |
应用持有的稳定状态;三种材质引用相同描述对象 |
attach(mesh) |
安装三种材质并保存原始引用,只支持单材质普通 Mesh |
detach(mesh) |
恢复该工厂仍在持有的材质槽,不销毁几何体或原材质 |
setThreshold(value) |
验证范围后更新 uniform,不重编译 |
clone() |
重新运行工厂,创建独立状态和新回调 |
dispose() |
先解除挂接,再释放该工厂创建的三种材质;重复调用无副作用 |
2. 结构配置和运行状态分离
工厂支持 bands 与 cells 两种模式,产生不同 GLSL 表达式,因此模式在构造时确定并进入缓存键:
target.customProgramCacheKey = () => `tutorial/dissolve-bundle/v1/${mode}/${pass}`;
pass 为 surface、depth、distance;Three.js 自己的键还包含内置材质类型及其他程序参数。这里额外写 pass 使自定义结构身份明确,不用 UUID 或时间制造不必要的程序。
threshold、edgeWidth、edgeColor 只改变数值,不改变声明和代码结构,所以是 uniform。切换结构模式时创建另一组效果,保持模块简单;需要大量频繁切换时可以扩展明确的 setMode,再统一管理更新和缓存签名。
3. 三个通道引用相同状态
const uniforms = {
uDemoThreshold: { value: 0.35 },
uDemoEdgeWidth: { value: 0.08 },
uDemoEdgeColor: { value: new THREE.Color(0xef8a45) }
};
// 对主材质、Depth、Distance 的每次注入:
// bindUniforms(shader, uniforms);
这里 THREE 是模块导入。三个通道会在不同时间、不同程序中使用这些引用。更新一次 threshold,主画面与阴影使用同一个阈值,不依赖保存“最近编译出来的 shader”。
某些 uniform 在深度程序中没有实际使用,被 GLSL 编译器优化掉是正常现象。它们出现在 JavaScript 的 uniforms 字典里,不代表一定占用 GPU uniform location。
工厂的遮罩使用原始局部 position 传给片元,三个通道基于同一个表达式 discard。threshold=1 显式完全丢弃,避免极值处残留;threshold=0 不丢弃。边缘发光只在表面材质执行,深度与距离输出保持原编码。
4. 一个可以直接运行的组合例子
将完整工厂源码、runtime.js、patch-utils.js 放在入口旁边,再运行:
import { createDemo } from './runtime.js';
import { createDissolveBundle } from './create-dissolve-bundle.js';
const { THREE, scene, renderer, onFrame, onDispose, dispose } = createDemo();
renderer.shadowMap.enabled = true;
const original = new THREE.MeshStandardMaterial({ color: 0x999999 });
const mesh = new THREE.Mesh(new THREE.SphereGeometry(1.4, 48, 32), original);
mesh.position.y = 1.5;
mesh.castShadow = true;
scene.add(mesh);
const effect = createDissolveBundle({ mode: 'bands', threshold: 0.3 });
effect.attach(mesh);
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(15, 15),
new THREE.MeshStandardMaterial({ color: 0x999083, roughness: 0.9 })
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground, new THREE.HemisphereLight(0xdde9ff, 0x443329, 1.2));
const light = new THREE.DirectionalLight(0xffffff, 3);
light.position.set(3, 6, 4);
light.castShadow = true;
light.shadow.camera.left = light.shadow.camera.bottom = -5;
light.shadow.camera.right = light.shadow.camera.top = 5;
light.shadow.camera.near = 0.1;
light.shadow.camera.far = 20;
light.shadow.camera.updateProjectionMatrix();
scene.add(light);
onFrame((dt, elapsed) => {
effect.setThreshold(0.5 + 0.45 * Math.sin(elapsed * 0.6));
});
// runtime 会收集当前 scene 中的主材质和自定义阴影材质。
// 原材质已不在场景中,由创建它的本例显式释放。
onDispose(() => {
effect.detach(mesh);
original.dispose();
});
if (import.meta.hot) import.meta.hot.dispose(dispose);
运行后可见球体与地面阴影同步产生条带裁切。用点光源替换方向光时,应开启点光源阴影并设置适当范围,检查距离通道。阴影参数和分辨率要按场景调整,不能把偏移误差都归因于遮罩。
本例使用 runtime 收集并释放当前场景资源,因此没有再注册 effect.dispose,避免重复管理。若在自己的应用中移除效果且继续使用场景,应调用 effect.dispose,它会恢复原材质;原材质与几何体仍由调用者负责。
5. 克隆真正需要复制什么
const independent = effect.clone() 会重建三种材质、GLSL 回调和 uniform 描述对象。两个效果可以共享同样的 GPU 程序结构,但 threshold 数值独立。
若只是 material.clone(),自定义回调与配套阴影材质不会自动变成完整效果副本。工厂 clone 只承诺复制它管理的配置和当前颜色/阈值等状态;你在外部任意修改的所有 Material 字段不一定在它的复制范围。需要扩展时应明确增加参数,而不是声称深复制一切。
一个 bundle 可以挂到多个 mesh,但它们共享状态。需要每个对象不同的动画进度,就创建独立 bundle,或另行设计实例 attribute,不要靠连续绘制共享内置材质时临时修改 uniform 来猜测上传行为。
6. 安装与释放都要有边界
attach 的 Map 保存原始引用,重复安装不会再包一层回调。detach 只恢复仍由本工厂占据的材质槽,避免覆盖外部后来安装的新材质。dispose 不释放模型几何体,也不释放原材质,因为它们不属于工厂。
本例不加载纹理,因此没有纹理所有权问题。若未来加入采样图,应在配置里明确“借用外部纹理”还是“内部创建并拥有”;不要因为 texture 被 uniform 引用就默认工厂有权销毁它。
7. 发布前检查
确认锚点与版本、声明无冲突、结构键完备、状态更新不重编译、克隆独立、主画面与两种阴影路径一致、目标几何特性受支持,并在卸载后检查材质与事件是否释放。对新增模式执行一次真实浏览器编译,静态字符串断言不能替代它。
练习(不附答案)
- 创建两组同模式 bundle,设置不同 threshold,验证视觉独立且程序结构可复用。
- 给工厂增加 setEdgeWidth,验证输入并确保改变宽度不会新增程序变体。
- 扩展 clone 的参数复制范围,让 roughness 也由工厂显式管理。
- 给一组效果先 attach、再 detach、最后 dispose,检查原材质恢复且模型几何体仍可使用。