本节摘要:实时阴影的本质是让场景从灯光视角再渲染一遍,把"离灯最近的可挡物"记录成一张深度图,正式渲染时逐像素对照这张图判断是否被挡。本节拆解这个两遍机制,给出 ShadowGenerator 的关键参数与伪影排查表,并为移动端准备降级方案。承接 2.2 的相机矩阵概念,是第3章的收官。
先回答一个少有人明说的问题:影子是物体的属性吗?不是。影子是"光源与遮挡物"的关系,引擎每帧要为这个关系额外画一遍场景。流程是:先把相机"搬"到灯光的位置朝照射方向看,把每个像素到灯的距离记进一张深度图(阴影贴图);正式渲染时,每个像素再算一次"我到灯的距离",与深度图对照——比记录值远,说明中间有东西挡着,判为阴影。
这就是为什么阴影贵:一盏投影灯等于场景多画一遍。也解释了两个经典现象——点光源没有天然影子(它朝六个方向照,理论上要六遍),以及影子边缘发虚(深度图分辨率有限,判定在边界处失准)。

方向光或聚光灯配一个生成器,投掷物登记进名单,接收面打开开关,影子就通了:
// 三件套:灯、生成器、登记 const sun = new BABYLON.DirectionalLight("sun", new BABYLON.Vector3(-0.5, -1, -0.3), scene); const shadowGen = new BABYLON.ShadowGenerator(1024, sun); // 深度贴图边长 1024 shadowGen.usePercentageCloserFiltering = true; // 软化边缘:观感与性能折中 heroMesh.receiveShadows = true; // 接收:地面上落别人的影子 shadowGen.addShadowCaster(heroMesh); // 投掷:自己挡光产生影子 ground.receiveShadows = true; // 常用微调三参数 shadowGen.bias = 0.0005; // 深度容差:条纹伪影的第一旋钮 shadowGen.normalBias = 0.01; // 沿法线偏移:细窄物体自阴影的解药 shadowGen.darkness = 0.4; // 0 全黑影到 1 几乎无影
两个名单要分开理解:投掷名单决定"谁挡光",接收开关决定"谁显示影子"。忘了登记投掷物,影子凭空消失;只投不收,物体挡了光但地面不显示——两个症状对应两个名单,排查不混淆。
动态物体每帧重画深度图理所当然;静态物体可以再省一步,把深度图冻结:
// 场景里只有一盏灯动、物体全静止:深度图只需更新一次 shadowGen.getShadowMap().refreshRate = BABYLON.RenderTargetTexture.REFRESHRATE_RENDER_ONCE; // 物体动了要手动重刷一次 shadowGen.getShadowMap().refresh();
阴影出问题有三种高发面孔,各自有固定排查路径。条纹伪影(表面出现斑马纹):bias 与 normalBias 双参联调,先加 normalBias 再动 bias。影子缺失:按"投掷名单、接收开关、灯的类型"三步查,点光源默认无投影是重灾区。影子与物体脱节(脚底悬空一大截):bias 加过头了,往回收。高频原因与解法整理成速查:
| 症状 | 高频原因 | 第一动作 |
|---|---|---|
| 表面条纹斑马纹 | 深度对照无容差 | 调大 normalBias 与 bias |
| 影子整体缺失 | 未登记投掷名单 | 补 addShadowCaster 调用 |
| 点光源无影 | 类型不支持单遍投影 | 换方向光或聚光灯 |
| 影子边缘锯齿 | 贴图分辨率不足 | 尺寸翻倍或开软化过滤 |
| 脚底影子脱开 | bias 过大 | 收小 bias 数值 |
移动端的降级顺序是:贴图尺寸降到 512、关闭软化过滤只留硬影、只保留主光投影、静态场景用烘焙思路——深度图刷一次后冻结,四步做完,中端手机也能带得动影子。
⚠️ 常见坑:阴影不是"场景级开关"而是逐灯逐网格的配置。给全部网格无差别登记投掷与接收,等于把每盏灯的深度图绘制范围拉满,帧率无声腰斩——只给视觉重点区域的物体开。
把参数表走成完整流程。背景:一个户外观展平台,白天场景,地面一片惨白,建筑像浮在纸面上没有重量感。第一轮先通路:加方向光与生成器,尺寸按桌面 1024 起,把主展馆与台阶登记进投掷名单、地面打开接收——影子立刻出现,但台阶立面上爬满斑马纹。第二轮治伪影:先把 normalBias 提到 0.02,条纹淡了大半;残余部分再微调 bias 到 0.0006,立面干净了。第三轮管观感与账本:硬影边缘生硬,桌面端开软化过滤,边缘自然但帧耗时涨了约 1.5 毫秒;中端手机吃不消,最终按设备分档——桌面开软化保 1024,移动端关软化降到 512 只留主展馆投影。结果:桌面稳 60 帧,移动端回到可玩的 30 帧档,画面成立。解读:调参顺序本身就是方法论——先灯型、再名单、再偏移、最后观感与分档,乱序会在症状间来回打转。变式:黄昏场景灯位压低、影子拉长,深度图精度不够时影缘出现格状断裂,思路是把阴影有效范围收窄到观众真正看得到的地带,远处放弃投影。
// 展馆阴影的调优骨架:先通路、再治伪影、后分档 const size = isMobile ? 512 : 1024; // 尺寸分档:移动端先砍一半 const shadow = new BABYLON.ShadowGenerator(size, sun); shadow.addShadowCaster(mainHall); // 只登记视觉重点物 shadow.addShadowCaster(stairs); ground.receiveShadows = true; shadow.normalBias = 0.02; // 第二轮:条纹的第一旋钮 shadow.bias = 0.0006; // 残余伪影再微调 shadow.usePercentageCloserFiltering = !isMobile; // 第三轮:观感按设备分档
第3章收束,画面有了颜色与影子。下一章回到帧的第1站:动画、交互、物理、界面与声音——渲染开始之前,世界先要动起来。