光照(Lighting)与阴影(Shadow)共同决定了场景的立体感与真实感。一个只有纹理没有光照的 3D 场景看起来像"贴纸",而阴影则是人类视觉系统判断空间关系的核心线索——没有阴影,物体仿佛悬浮在空中。实时渲染经过四十年的演进,形成了从简单经验模型(Phong)到物理基渲染(PBR)的光照理论,以及从平面阴影贴图到级联阴影、PCSS 软阴影的完整技术栈。本文按"从简单到工业级"的顺序,系统梳理这些技术背后的数学原理与工程实践。
一、经典光照模型:从 Lambert 到 Blinn-Phong
一句话:经典光照模型将光照拆解为环境光、漫反射、镜面高光三项,用少量参数快速近似真实光学,是理解 PBR 的跳板。
1.1 Lambert 漫反射
Lambert 模型假设表面是理想漫反射体(朗伯体),反射光强度与入射光和法线夹角的余弦成正比,与观察方向无关。其 BRDF 是一个常量 ρ / π,能量守恒下任意方向反射率总和为 1。
L_diffuse = kd * albedo * max(dot(N, L), 0.0) * lightColor
max(dot(N, L), 0.0) 称为 N·L 项,是实时渲染中最常见的几何项。它意味着当光与表面接近平行时,光照贡献趋近于零——这也是正午阳光与清晨阳光亮度差异的来源。
1.2 Phong 高光
Phong 在漫反射基础上引入镜面高光:当观察方向 V 接近光线的反射方向 R 时,看到强烈高光。反射向量计算如下:
R = 2 * dot(N, L) * N - L
spec = ks * pow(max(dot(R, V), 0.0), shininess)
shininess(镜面指数)控制高光锐度:指数越大,高光斑越小越亮。Phong 的缺点是需要每像素计算反射向量 R,且当 N·L < 0 时(光在表面背面)反射向量方向反转,会产生不自然的亮点。
1.3 Blinn-Phong 半程向量
Blinn-Phong 用半程向量 H = normalize(L + V) 代替反射向量 R,高光变为:
H = normalize(L + V)
spec = ks * pow(max(dot(N, H), 0.0), shininess)
半程向量是 L 与 V 的角平分线,物理含义是"若存在一个微表面法线恰好指向 H,则会产生镜面反射"。Blinn-Phong 更高效、更稳定,是 2000 年代游戏渲染的事实标准。
1.4 光照模型对比
| 模型 | 数学复杂度 | 高光质量 | 物理真实性 | 适用场景 |
|---|---|---|---|---|
| Lambert | 低 | 无高光 | 仅漫反射 | 白模、预览、AO 渲染 |
| Phong | 中 | 依赖 R 计算 | 经验模型 | 老式引擎、卡通渲染 |
| Blinn-Phong | 中 | H 计算更稳定 | 经验模型 | 移动端前向渲染 |
| Cook-Torrance (PBR) | 高 | 能量守恒、金属度 | 物理基 | 现代引擎标准 |
// GLSL:Blinn-Phong 片段着色器核心
uniform vec3 uLightDir;
uniform vec3 uViewPos;
uniform vec3 uLightColor;
vec3 blinnPhong(vec3 N, vec3 V, vec3 albedo, float shininess) {
vec3 L = normalize(uLightDir);
vec3 H = normalize(L + V);
float diff = max(dot(N, L), 0.0);
float spec = pow(max(dot(N, H), 0.0), shininess);
vec3 ambient = 0.05 * albedo; // 环境项(粗略近似)
vec3 diffuse = albedo * diff;
vec3 specular = vec3(1.0) * spec;
return ambient + (diffuse + specular) * uLightColor;
}
二、PBR 光照:Cook-Torrance BRDF
一句话:PBR 用微表面理论 + 能量守恒的 Cook-Torrance BRDF 替代经验模型,将材质参数抽象为 Albedo、金属度、粗糙度三个物理量。
2.1 微表面理论
真实表面在微观尺度上是凹凸不平的,每个微平面都是理想镜面反射体。BRDF 描述的是这些微平面统计反射的结果。Cook-Torrance 将反射分解为漫反射项与镜面反射项:
f = kd * (albedo / π) + kspec * (DFG / (4 * (N·V) * (N·L)))
三项关键函数:
- D(法线分布函数 NDF):微表面法线朝向半程向量 H 的概率。GGX/Trowbridge-Reitz 是实时渲染标准:
D = α² / (π * ((N·H)² * (α² - 1) + 1)²) 其中 α = roughness² - G(几何遮蔽函数):微表面间的自遮挡。Smith 方法 + Schlick-GGX 近似,加上可见性项
V = G / (4·(N·V)·(N·L))。 - F(菲涅尔项):反射率随视角变化,Schlick 近似:
F = F0 + (1 - F0) * pow(1 - (V·H), 5) F0 = 0.04(电介质)或 albedo(金属)
2.2 金属度工作流
金属度(metallic)将材质分为两类:金属(漫反射为 0,镜面反射带颜色 F0 = albedo)与电介质(F0 ≈ 0.04,漫反射 albedo)。关键实现点是 F(菲涅尔)本身就是 kS(镜面反射占比),漫反射 kD = 1 - kS 后再乘 (1 - metallic)。
// GLSL:Cook-Torrance PBR 光照
const float PI = 3.14159265359;
float distributionGGX(vec3 N, vec3 H, float roughness) {
float a = roughness * roughness;
float a2 = a * a;
float NdotH = max(dot(N, H), 0.0);
float d = (NdotH * NdotH * (a2 - 1.0) + 1.0);
return a2 / (PI * d * d);
}
float geometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
float k = (roughness + 1.0) * (roughness + 1.0) / 8.0;
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float g1 = NdotV / (NdotV * (1.0 - k) + k);
float g2 = NdotL / (NdotL * (1.0 - k) + k);
return g1 * g2;
}
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
vec3 pbrDirectLight(vec3 N, vec3 V, vec3 L, vec3 albedo,
float metallic, float roughness) {
vec3 H = normalize(V + L);
float NDF = distributionGGX(N, H, roughness);
float G = geometrySmith(N, V, L, roughness);
vec3 F = fresnelSchlick(max(dot(H, V), 0.0), mix(vec3(0.04), albedo, metallic));
vec3 kS = F;
vec3 kD = (1.0 - kS) * (1.0 - metallic);
vec3 specular = (NDF * G * F) / (4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001);
float NdotL = max(dot(N, L), 0.0);
return (kD * albedo / PI + specular) * NdotL;
}
2.3 IBL 补充
现代 PBR 还需基于图像的光照(IBL):环境贴图经预过滤(Pre-filtered Mipmap)对应不同粗糙度,漫反射部分用辐照度图(Irradiance Map)。镜面 IBL 通过 split-sum 近似,将 BRDF 积分拆分为"预过滤环境光 × 预计算 BRDF 查表",在实时中一次纹理采样完成。IBL 的引入使得物体天然继承环境光照,是 PBR 工作流不可或缺的一环。
三、光源类型与衰减
一句话:实时引擎的光源按几何形态分为平行光、点光、聚光灯,其区别在于方向向量与衰减函数的差异。
| 光源类型 | 方向/位置 | 衰减 | 典型用途 |
|---|---|---|---|
| 平行光 (Directional) | 方向固定、无位置 | 无衰减 | 太阳、月亮 |
| 点光 (Point) | 有位置、全方位 | 1 / (d²) 或逆向平方 | 灯泡、火把 |
| 聚光灯 (Spot) | 有位置、有锥角 | 位置衰减 × 角衰减 | 手电、舞台灯 |
| 面积光 (Area) | 面状几何 | 软阴影、昂贵 | 影视级室内 |
点光理想衰减为逆向平方律 1 / d²,但纯逆向平方在 d → 0 时会发散到无穷大,实时实现通常用有界形式:
// GLSL:有界衰减(避免 d→0 发散)
float attenuation(float dist, float lightRadius) {
float d = dist / lightRadius; // 归一化距离
return clamp(1.0 / (d * d + 1.0), 0.0, 1.0);
// 也可用 smoothstep 过渡:saturate(1.0 - (d*d) ),常见于移动端
}
聚光灯在点光基础上增加角衰减——从光锥中心到边缘的平滑过渡:
// GLSL:聚光灯角度衰减
float spotAtten(vec3 L, vec3 spotDir, float innerCos, float outerCos) {
float cosAngle = dot(normalize(-L), spotDir); // 注意方向取反
return smoothstep(outerCos, innerCos, cosAngle); // 边缘→中心渐变
}
光照累加时务必在**线性空间(HDR)**进行:颜色先反 Gamma(sRGB → linear),累加完再做色调映射与 Gamma 编码,否则物理强度无法正确叠加,高光区域会出现灰暗(banding)问题。
四、阴影贴图(Shadow Map)原理
一句话:阴影贴图从光源视角渲染一张深度图,遮挡测试退化为一次深度比较——简单却存在分辨率与精度两大瓶颈。
阴影贴图(Shadow Mapping)是最主流的实时阴影技术,其流程分为两步:
- Light Pass:以光源位置为视点、光源方向为视线方向,渲染场景到一张深度纹理(Shadow Map),记录每个 texel 处离光源最近的深度。
- Main Pass:从相机视角渲染像素时,将该像素变换到光源空间,比较其"光空间深度"与阴影贴图中记录的值:若更远则被遮挡(阴影中),否则可见。
// GLSL:Main Pass 中的阴影采样
uniform sampler2D uShadowMap;
uniform mat4 uLightSpaceMatrix;
float shadowCalculation(vec4 fragPosLightSpace) {
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w; // 透视除法 → NDC
projCoords = projCoords * 0.5 + 0.5; // NDC [-1,1] → UV [0,1]
float closestDepth = texture(uShadowMap, projCoords.xy).r;
float currentDepth = projCoords.z;
// 深度偏移避免自遮挡(shadow acne)
float bias = max(0.002 * (1.0 - dot(normal, lightDir)), 0.001);
return currentDepth - bias > closestDepth ? 1.0 : 0.0;
}
关键的实现细节:
- 透视除法:光照投影矩阵若是透视投影(点光/聚光灯),需除以 w;平行光用正交投影,w = 1。
- 深度偏移(Depth Bias):由于深度纹理精度有限,被照亮面自身采样时会出现深度"自遮挡"噪声,称为阴影粉刺(Shadow Acne),用 bias 推开。
- 采样器比较:现代 API 可用
VK_COMPARE_OP_LESS+shadow2DProj/texture()的比较模式采样,返回硬布尔结果,无需手动分支。
五、阴影质量问题:锯齿、Peter Panning 与级联
一句话:阴影贴图的两大经典 artifact——锯齿(分辨率不足)与 Peter Panning(bias 过大导致阴影脱离接触点)——本质上是分辨率和精度的此消彼长。
5.1 锯齿(Aliasing)与 PCF
硬阴影在低分辨率下呈现严重锯齿。PCF(Percentage-Closer Filtering) 的思路是:不采样单个 texel,而是对周围 3×3 / 5×5 邻域做遮挡比较后取平均,产生柔和边缘:
// HLSL:PCF 软边缘(与硬件 PCF 采样器异曲同工)
float shadowPCF(float4 shadowCoord, float depth) {
float2 texelSize = 1.0 / float2(textureSize(shadowMap, 0));
float shadow = 0.0;
[unroll]
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
float2 uv = shadowCoord.xy + float2(x, y) * texelSize;
shadow += (depth > shadowMap.SampleCmp(sampler, uv, depth)) ? 1.0 : 0.0;
}
}
return shadow / 9.0;
}
硬件支持比较采样时(SampleCmp / texture() with compare sampler),过滤由固定功能单元完成,性能远优于 9 次手动比较。
5.2 Peter Panning 与深度偏移的矛盾
bias 过小 → 阴影粉刺;bias 过大 → 阴影"飘"离物体,出现悬空阴影(Peter Panning)。这是因为 bias 相当于把被照亮面在深度上"抬高",抬得越多,接触阴影越远离遮挡体。解决方案:
- Slope-Scale Bias:bias 随表面与光线的夹角变化(
dot(N, L)越小 bias 越大),配合深度纹理精度使用。 - Normal Offset:沿法线方向推开采样点(PCF 时尤其重要)。
- Z-fighting 检查:阴影场景若大面积自遮挡,优先怀疑 bias 方向或深度格式精度(
D16→D32F)。
5.3 单一贴图在大型场景中的失效
一个固定分辨率的阴影贴图(如 2048²)覆盖整片地图时,近处每像素对应的阴影 texel 可能超过 1 个屏幕像素(欠采样锯齿),远处则分辨率浪费。这就引出了级联阴影。
六、级联阴影(Cascaded Shadow Maps, CSM)
一句话:CSM 把视锥按深度切成多个级联,每个级联用独立的高分辨率阴影贴图覆盖,让近处阴影清晰、远处阴影覆盖范围大。
CSM 是目前大型场景阳光阴影的标准方案。其核心步骤:
- 分割视锥:将主相机视锥沿深度分成 3
4 段。分割点常用对数-线性混合公式:0.8,兼顾近处精度与远处覆盖。dist = lerp(log_dist, lin_dist, lambda),λ 通常取 0.5 - 每级联构建光源正交投影:计算该级联子视锥的世界空间包围盒,构建光源空间的正交矩阵,将场景渲染到该级联对应的阴影贴图。
- 采样时选择级联:根据像素深度确定所属级联,采样对应贴图;级联交界处按权重混合避免接缝。
// C++:计算级联分割距离
std::vector<float> computeCascadeSplits(int count, float nearZ, float farZ, float lambda) {
std::vector<float> splits(count + 1);
splits[0] = nearZ;
for (int i = 1; i <= count; i++) {
float t = (float)i / (float)count;
float logSplit = nearZ * std::pow(farZ / nearZ, t); // 对数分布:近处密集
float linSplit = nearZ + (farZ - nearZ) * t; // 线性分布
splits[i] = std::lerp(linSplit, logSplit, lambda); // 加权混合
}
return splits;
}
6.1 级联稳定与 Texel Snapping
相机移动时,若不锁定阴影贴图 texel 与场景的对应关系,阴影会"漂移抖动"。经典解决法是Texel Snapping:把光源正交矩阵的平移量对齐到 texel 尺寸的整数倍:
// C++:Texel Snapping 稳定阴影
glm::mat4 snapLightMatrix(const glm::mat4& lightViewProj, float shadowMapSize) {
glm::mat4 proj = lightViewProj;
float texelSize = 2.0f / shadowMapSize; // NDC 范围 [-1,1] 的 texel 尺寸
glm::vec4 origin = proj * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f);
glm::vec2 snapped = glm::floor(glm::vec2(origin) / texelSize) * texelSize;
proj[3][0] -= (snapped.x - origin.x);
proj[3][1] -= (snapped.y - origin.y);
return proj;
}
6.2 级联参数配置参考
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 级联数量 | 3~4 | 3 级兼顾质量与开销,4 级用于大型开放世界 |
| 贴图分辨率 | 1024² ~ 4096²/级 | 主级联 2048² 起,与屏幕像素 1:1 匹配 |
| 分割 λ | 0.5 ~ 0.8 | 越大近处越密、远处越稀 |
| 深度格式 | D32_FLOAT | 大深度范围下精度优于 D24 |
| Blend 过渡带 | 2~3 texel | 级联间 blend 平滑 |
七、软阴影与 PCSS
一句话:真实光源是面光源,阴影边缘必然柔和;PCSS 用"遮挡深度均值 → 估算半影大小 → 自适应 PCF 半径"模拟物理软阴影。
PCF 的滤波半径是固定的,因此远近阴影边缘硬度一致。真实场景中,物体离地面越远,半影(Penumbra)越宽。PCSS(Percentage-Closer Soft Shadows) 分三步解决:
- 遮挡搜索:在给定半径内采样阴影贴图,统计比当前深度更近的遮挡者,计算其平均深度
d_blocker。 - 半影估计:由相似三角形推得滤波半径
w_penumbra = (d_receiver - d_blocker) / d_blocker * w_light,物体越远半影越大。 - PCF 滤波:用上述半径做 PCF,得到软阴影边缘。
// GLSL:PCSS 遮挡搜索
float findBlocker(sampler2D shadowMap, vec2 uv, float zReceiver) {
float blockerDepth = 0.0;
int blockerCount = 0;
vec2 texelSize = 1.0 / vec2(textureSize(shadowMap, 0));
for (int x = -2; x <= 2; x++) {
for (int y = -2; y <= 2; y++) {
float d = texture(shadowMap, uv + vec2(x, y) * texelSize).r;
if (d < zReceiver) { // 该 texel 是遮挡者
blockerDepth += d;
blockerCount++;
}
}
}
return blockerCount == 0 ? -1.0 : blockerDepth / float(blockerCount);
}
PCSS 每像素采样次数较多(遮挡搜索 + 自适应 PCF),常用半分辨率计算再上采样、或与 TAA 时序累积配合,以控制性能。
其他软阴影方案
- VSM / EVSM(方差阴影贴图):存储深度与深度平方,用 Chebyshev 不等式求遮挡概率,天然可过滤,但易漏光(Light Leaking)。
- MSM(矩阴影贴图):VSM 改进,用更高阶矩减少漏光。
- 光线追踪阴影:直接对光源发射阴影射线,精度最高,见 光线追踪与混合渲染。
八、光照与阴影的工程实践
一句话:光照与阴影的选择是渲染架构的一环——前向渲染适合少光源 + MSAA,延迟渲染适合大量光源 + CSM + 后处理。
8.1 前向 vs 延迟的阴影集成
- 前向渲染(Forward):每个物体一次 pass,阴影贴图直接采样。半透明与 MSAA 天然支持,但光源多了开销爆炸。
- 延迟渲染(Deferred):G-Buffer 存储几何信息,光照 pass 全屏处理。CSM 采样在光照 pass 进行,每个像素只执行一次级联选择;配合 Tiled/Clustered 光源剔除,可承载数百光源。
8.2 性能预算
| 技术 | 主要开销 | 优化策略 |
|---|---|---|
| Shadow Map 渲染 | 每光源一次场景 pass | 只渲不透明物体、级联间共享渲染 |
| PCF / PCSS | 多次阴影采样 | 半分辨率 + 上采样、硬件比较采样 |
| CSM | 4 个 shadow pass | 视锥剔除、动态分辨率、级联数自适应 |
| PBR 直接光 | BRDF 函数计算 | 预计算 IBL、half precision、移动端降级 |
8.3 移动端与 Web 的取舍
移动端(以及 WebGPU)倾向:2 级 CSM + 128²~512² 阴影贴图 + 3×3 PCF,PBR 用简化的 GGX 或 Blinn-Phong;避免大半径 PCF 与 PCSS。桌面端则可用 2048²+ 4 级 CSM + PCSS + 光线追踪阴影。
总结
实时光照与阴影经过了"经验模型 → 物理模型 → 高精度阴影"的三级跳。Blinn-Phong 提供廉价的可读性,Cook-Torrance PBR 提供物理一致性与艺术统一性,阴影贴图家族(PCF/CSM/PCSS)则在分辨率、精度与性能之间做精细权衡。
| 层次 | 核心技术 | 工程目标 |
|---|---|---|
| 光照模型 | Lambert / Blinn-Phong / Cook-Torrance | 物理正确 + 艺术可控 |
| 光源管理 | 平行光/点光/聚光灯衰减、HDR 线性空间 | 多光源叠加不溢出 |
| 基础阴影 | Shadow Map + Bias + PCF | 无粉刺、无 Peter Panning |
| 大规模阴影 | CSM + Texel Snapping + 级联混合 | 大场景远近皆清晰 |
| 软阴影 | PCSS / VSM / RT shadow | 半影随距离变化 |
学习建议:先在 GLSL 里用 Blinn-Phong 点亮一个场景,再逐函数替换为 Cook-Torrance 并加入 IBL;阴影部分从单贴图加 PCF 起步,逐步演进到 CSM 与 PCSS。每一步都建议用渲染截图对比 artifact 变化,阴影调试是培养图形学直觉的最佳实践。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。