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. 阴影仍然不对时检查什么

  1. 开启 renderer.shadowMap.enabled,确认光源 castShadow、物体 castShadow 和地面 receiveShadow。
  2. 检查光源 shadow camera 是否包住变形后的范围;扩大几何包围体也不能替代调整光源相机。
  3. 如果关闭了阴影自动更新,时间变化后还要使阴影图重新渲染。只改 uniform 不会强迫被冻结的阴影图更新。
  4. 在变形一致之后再调整 bias、normalBias;它们用于采样误差,不会修复位置公式错误。
  5. r185 的点光阴影不支持 VSM;点光测试选 PCF 或 Basic,别把算法不支持误认为补丁失败。

5. 生命周期与性能

depth、distance 是额外材质资源,移除 Mesh 后需显式 dispose()。若资源被多个 Mesh 共享,应由拥有者在最后一个使用者释放后统一销毁。克隆 Mesh 后也要重新确认这些引用及 uniform 是否应共享。

本系列 runtime 会统一收集并释放 Mesh 的自定义阴影材质,因此完整场景给 attachDeformationShadows 传第五个参数 false,让返回函数只解除引用;脱离运行壳时默认 true,返回函数同时释放资源。不要让运行壳和回调重复承担同一资源的销毁职责。

每个投影光源都可能再次执行顶点变形。点光源还涉及多个方向的阴影渲染。主画面速度不错不代表打开多个动态阴影后依然便宜;应分别比较关闭阴影、单灯和多灯的成本。

练习

  1. 在水面案例里移除 customDepthMaterial,固定时间截图比较阴影轮廓。
  2. 把 DirectionalLight 改为 PointLight,验证 customDistanceMaterial 的必要性。
  3. 冻结阴影自动更新,设计“仅在时间或幅度变化时刷新”的机制。
  4. 给主材质增加圆孔 discard,并同步两个阴影材质的覆盖逻辑。