24 · 让阴影跟随自定义变形
阴影并不是主相机颜色画面的复制。渲染器从光源视角把几何体再画一遍,记录深度或距离,然后在主材质中比较表面和阴影图。主材质的 onBeforeCompile 不会自动安装到这一轮使用的材质上。
1. 两种阴影材质
本项目 r185 的 WebGLShadowMap.getDepthMaterial() 根据 light.isPointLight 选择:
| 光源 | Mesh 自定义入口 | 材质 | 主要记录内容 |
|---|---|---|---|
| DirectionalLight、SpotLight | customDepthMaterial |
MeshDepthMaterial | 光源相机对应的深度 |
| PointLight | customDistanceMaterial |
MeshDistanceMaterial | 表面到光源的距离 |
自定义材质放在 Mesh 上,不是放在 material 上。只设置 customDepthMaterial,点光源阴影仍然不会使用这段补丁。castShadow 决定物体是否投影,receiveShadow 决定它是否在主材质中接收阴影;两者互不替代。
r185 的默认深度材质是 new MeshDepthMaterial(),点光距离 shader 在 ShaderLib/distance.glsl.js。不要将旧版本文章里的 RGBA 打包、点光阴影图布局和输出格式原封不动搬过来。此处仅修改几何位置,保留版本自身的深度/距离输出。
2. 共享函数和共享 uniform 包装对象
import { installDeformation, attachDeformationShadows } from './examples/deformation.js';
const state = {
uDemoTime: { value: 0 },
uDemoAmplitude: { value: 0.35 },
uDemoFrequency: { value: 2 },
uDemoSpeed: { value: 1.6 }
};
installDeformation(mesh.material, state, 'water');
const releaseShadows = attachDeformationShadows(THREE, mesh, state, 'water');
mesh.castShadow = true;
// 每帧仅更新一次。主画面和阴影引用同一个 { value }。
state.uDemoTime.value = elapsedSeconds;
// Mesh 不再使用时执行 releaseShadows()。
deformation.js 的安装器让三个 shader 都执行 demoDeform(),但阴影材质不安装可见材质的法线修正。原因是这里没有 displacementMap,阴影只需准确的几何位置;depth/distance 的 beginnormal_vertex 又位于 USE_DISPLACEMENTMAP 条件内,不能假设它与 Standard 的法线流程一致。
如果某种变形本身依赖法线或内置 displacementMap,就必须保证阴影 shader 也具备这些输入,并按照其真实代码顺序处理。
3. 透明覆盖也必须一致
一片叶子用 alpha 纹理切出轮廓,或者一个物体正在溶解,这些 discard 条件同样影响阴影。只同步顶点会得到“有洞的物体投出完整实心影子”。
内置的 map、alphaMap、alphaTest 等行为由阴影系统按其源码规则同步处理,但自定义的 uniform、噪声和 discard 不会被自动理解。把相同坐标生成、采样、阈值及 discard 补丁安装到 depth 和 distance fragment shader,才是完整解决办法。溶解案例 提供了这种实现。
不要把可见材质的整份 fragment shader 复制到深度材质:那会丢掉深度或距离输出,也引入无意义的光照计算。共享决定覆盖范围的逻辑即可。
4. 阴影仍然不对时检查什么
- 开启
renderer.shadowMap.enabled,确认光源castShadow、物体castShadow和地面receiveShadow。 - 检查光源 shadow camera 是否包住变形后的范围;扩大几何包围体也不能替代调整光源相机。
- 如果关闭了阴影自动更新,时间变化后还要使阴影图重新渲染。只改 uniform 不会强迫被冻结的阴影图更新。
- 在变形一致之后再调整 bias、normalBias;它们用于采样误差,不会修复位置公式错误。
- r185 的点光阴影不支持 VSM;点光测试选 PCF 或 Basic,别把算法不支持误认为补丁失败。
5. 生命周期与性能
depth、distance 是额外材质资源,移除 Mesh 后需显式 dispose()。若资源被多个 Mesh 共享,应由拥有者在最后一个使用者释放后统一销毁。克隆 Mesh 后也要重新确认这些引用及 uniform 是否应共享。
本系列 runtime 会统一收集并释放 Mesh 的自定义阴影材质,因此完整场景给 attachDeformationShadows 传第五个参数 false,让返回函数只解除引用;脱离运行壳时默认 true,返回函数同时释放资源。不要让运行壳和回调重复承担同一资源的销毁职责。
每个投影光源都可能再次执行顶点变形。点光源还涉及多个方向的阴影渲染。主画面速度不错不代表打开多个动态阴影后依然便宜;应分别比较关闭阴影、单灯和多灯的成本。
练习
- 在水面案例里移除 customDepthMaterial,固定时间截图比较阴影轮廓。
- 把 DirectionalLight 改为 PointLight,验证 customDistanceMaterial 的必要性。
- 冻结阴影自动更新,设计“仅在时间或幅度变化时刷新”的机制。
- 给主材质增加圆孔 discard,并同步两个阴影材质的覆盖逻辑。