在做三维可视化项目时,经常遇到画面看起来“平”、缺乏真实感的问题。比如一个办公室场景,桌椅模型都很精细,但就是不像真的——原因往往出在光影上。没有影子的物体就像漂浮在空中,观众一眼就能看出是假的。这时候,把阴影映射集成到渲染管线就成了关键一步。
什么是阴影映射
简单说,阴影映射是一种让3D场景中物体产生阴影的技术。它通过从光源视角先渲染一次场景,记录每个点的距离(深度),生成一张“深度图”。之后在主摄像机视角渲染时,对比当前点是否比“深度图”中的对应位置更远,来判断它是否处于阴影中。
为什么需要集成进渲染管线
很多初学者会单独跑一遍阴影计算,再叠加到最终图像上,结果要么性能差,要么边缘不匹配。现代图形引擎的做法是把阴影计算作为渲染流程的一个环节,像流水线一样串联起来。这样GPU能高效调度资源,避免重复绘制和内存拷贝。
典型的集成步骤
以一个基于OpenGL或WebGL的办公家具预览系统为例:
- 先用平行光视角渲染所有物体,输出深度纹理(即Shadow Map)
- 切换到主相机视角,正常进行逐像素光照计算
- 在片段着色器中采样Shadow Map,做一次深度比较
- 根据比较结果调整最终亮度
这个过程必须和现有的材质系统、光照模型协调工作,否则会出现某些材质有影、某些没影的情况。
代码示意
下面是片段着色器中常见的阴影判断逻辑:
// 将世界坐标转换到光源的裁剪空间
vec4 lightSpacePos = lightMatrix * vec4(worldPos, 1.0);
// 转换为标准设备坐标
vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w;
projCoords = projCoords * 0.5 + 0.5;
// 采样深度图
float closestDepth = texture(shadowMap, projCoords.xy).r;
float currentDepth = projCoords.z;
// 比较深度,加入偏移防止自阴影错误
float bias = 0.005;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
// 应用到光照结果
vec3 lighting = (1.0 - shadow) * (diffuse + specular);
实际应用中的小坑
在开发远程协作白板这类实时渲染工具时,曾遇到过阴影闪烁的问题。排查发现是Shadow Map分辨率太低,加上相机移动导致深度采样跳变。解决办法是提升纹理尺寸,并加入PCF(百分比渐近过滤)做软化处理。
还有一次,UI元素被错误地投了影。原因是渲染顺序没控制好,2D界面也被送进了阴影生成阶段。后来通过分层渲染,给不同对象打标签,才彻底解决。
性能考量
每增加一盏阴影灯,就要多一次深度渲染。对于移动端或网页端应用,建议限制同时启用的阴影光源数量。可以优先给主光源加影,辅助光只负责补亮,不参与阴影计算。
另外,使用级联阴影(CSM)能在大场景中平衡远近区域的精度,特别适合展示整层办公楼的虚拟导览系统。