Deep Research By Google Gemini

:) TLDR, 直接查看交互网站 link

引言

阴影的必要性

在实时三维计算机图形学中,阴影并非简单的视觉点缀,而是构建可信、沉浸式虚拟世界的基石 1。动态阴影为场景提供了至关重要的深度感和空间一致性线索,使用户能够直观地理解物体之间的相对位置、距离和场景的整体光照环境 2。一个没有阴影的场景往往显得扁平、不真实,物体如同悬浮在空中。随着现代游戏引擎越来越多地采用完全动态的光照和交互式环境,对高效、逼真且可扩展的实时阴影解决方案的需求已变得空前迫切 3

现代渲染图景

游戏图形技术已经从主要依赖预计算(烘焙)静态光照的时代,演进到了一个以动态光照和材质为主导的新阶段。玩家期望看到动态变化的时间周期、可破坏的环境以及与光照实时交互的角色和物体。这种转变使得传统的静态光照贴图(Lightmap)技术捉襟见肘,实时动态阴影渲染成为了现代渲染管线的核心挑战之一 3

报告目标与用户背景

本报告旨在为图形程序员和引擎开发者提供一份关于现代实时阴影算法的全面、深入的技术指南。我们将系统性地剖析各种主流及前沿阴影技术的原理、实现细节、性能特征以及视觉效果。报告将特别关注在Windows平台下,以DirectX 12(DX12)作为渲染硬件接口(RHI)的实现上下文,这与您的开发环境完全契合。

当前,存在着多种阴影渲染算法,从近似但高效的方法到精确但计算密集的方法,它们各自适用于不同的光源类型、场景规模和性能预算 4。对于开发者而言,理解这些技术的优缺点、局限性及适用场景,是做出正确技术选型、构建稳定高效阴影系统的关键。本报告将为您梳理这一复杂的技术图谱,助您将引擎中尚待启用的阴影功能,转化为一套真正健壮、高质量且可投入生产的解决方案。

第一部分:奠基石——阴影贴图(Shadow Mapping)

阴影贴图(Shadow Mapping, SM)是绝大多数现代光栅化阴影技术的基础。深刻理解其核心原理及其固有的局限性,是掌握后续更高级算法演进逻辑的先决条件。

1.1. 核心原理:从光源视角进行渲染

阴影贴图技术的核心思想源于一个简单的物理直觉:一个点如果能被光源直接“看到”,那么它就是被照亮的;反之,如果它被其他物体遮挡,那么它就处于阴影之中。这一概念在图形学中被称为“阴影/视图二元性”(Shadow/View Duality)5

该技术通过一个经典的两遍(Two-Pass)算法实现 1

  1. 第一遍(阴影贴图生成)
    • 将虚拟“摄像机”放置在光源的位置和方向,渲染整个场景。
    • 在此过程中,我们不关心物体的颜色信息,只关心深度。渲染管线会将每个片元(fragment)距离光源的深度值写入一个专门的纹理中。这个纹理就是阴影贴图(Shadow Map),或称为深度图(Depth Map)1
    • 为了优化性能,这一遍通常会禁用颜色缓冲区的写入,因为内存带宽往往是GPU的主要性能瓶颈之一 6。在DirectX 12中,这可以通过在 D3D12_GRAPHICS_PIPELINE_STATE_DESC中设置一个空(null)的像素着色器(Pixel Shader)来实现。
  2. 第二遍(场景渲染与阴影测试)
    • 将摄像机移回玩家的正常视点,正常渲染场景。
    • 对于场景中的每一个被渲染的片元,首先计算其在世界空间中的位置。
    • 然后,使用与第一遍中完全相同的光源视图-投影矩阵,将该片元的世界坐标变换到光源的裁剪空间(Light’s Clip Space)。
    • 将变换后的坐标进行透视除法,得到其在光源视角下的归一化设备坐标(NDC)和深度值。NDC的XY分量用于在阴影贴图中采样,Z分量则代表了该片元在光源空间中的深度。
    • 采样阴影贴图,获取在相同XY坐标下,离光源最近的物体的深度值(即第一遍中存储的值)。
    • 比较当前片元的光源空间深度与从阴影贴图中采样出的深度。如果当前片元的深度大于采样深度,说明在它与光源之间存在一个更近的物体(遮挡物),因此该片元处于阴影中;否则,它被照亮 1

以下是一个简化的HLSL(High-Level Shading Language)代码片段,用于说明其核心逻辑:

// 全局常量
cbuffer ShadowConstants
{
    matrix g_LightViewProj; // 光源的视图-投影矩阵
};

Texture2D g_ShadowMap : register(t0);
SamplerState g_ShadowSampler : register(s0);

// =================================================
// Pass 1: 阴影贴图生成
// =================================================

// 顶点着色器 (VS)
struct VS_SHADOW_OUTPUT
{
    float4 Position : SV_Position;
};

VS_SHADOW_OUTPUT ShadowMapVS(float4 WorldPos : POSITION)
{
    VS_SHADOW_OUTPUT output;
    // 将顶点变换到光源的裁剪空间
    output.Position = mul(g_LightViewProj, WorldPos);
    return output;
}

// 像素着色器 (PS)
// 在DX12中,此阶段可以设置为nullptr来仅进行深度写入。
// 如果需要写入特定格式(如线性深度),则需要一个简单的PS。
float4 ShadowMapPS(VS_SHADOW_OUTPUT input) : SV_Target
{
    // 不需要输出颜色,仅深度缓冲起作用。
    // 或者,对于某些技术,可以返回深度值到颜色缓冲。
    return float4(input.Position.z / input.Position.w, 0, 0, 1);
}

// =================================================
// Pass 2: 场景渲染与阴影测试
// =================================================

struct VS_SCENE_OUTPUT
{
    float4 PositionCS : SV_Position;   // 摄像机裁剪空间位置
    float4 PositionLS : TEXCOORD0;     // 光源裁剪空间位置
    //... 其他插值,如法线、纹理坐标等
};

// 场景渲染的像素着色器 (PS)
float4 ScenePS(VS_SCENE_OUTPUT input) : SV_Target
{
    //... 省略常规光照计算...

    // 1. 变换到光源的NDC空间
    float3 shadowCoord = input.PositionLS.xyz / input.PositionLS.w;

    // 2. 将NDC坐标从[-1, 1]映射到纹理坐标
    // 注意:DX的纹理坐标原点在左上角
    shadowCoord.x = shadowCoord.x * 0.5f + 0.5f;
    shadowCoord.y = shadowCoord.y * -0.5f + 0.5f;

    // 3. 采样阴影贴图
    float closestDepth = g_ShadowMap.Sample(g_ShadowSampler, shadowCoord.xy);

    // 4. 深度比较
    float currentDepth = shadowCoord.z;
    float shadowFactor = (currentDepth > closestDepth)? 0.0f : 1.0f;

    // 将阴影因子应用到最终颜色
    float3 finalColor = AmbientColor + shadowFactor * (DiffuseColor + SpecularColor);
    return float4(finalColor, 1.0f);
}

1.2. 固有的瑕疵与缓解策略

阴影贴图的简洁性是以一系列固有的视觉瑕疵为代价的。理解这些瑕疵的根源至关重要,因为它们并非“bug”,而是该近似算法的内在属性。

  • 分辨率走样(“Jaggies”): 这是最根本的问题。阴影贴图是一个离散的像素网格。如果阴影贴图的分辨率相对于其在屏幕上覆盖的面积过低,其块状的像素结构就会在阴影边缘表现为锯齿状,即“Jaggies”7。这是一种典型的放大瑕疵(magnification artifact),当光源的投影被放大显示在屏幕上时尤为明显7
  • 深度精度病症
    • 阴影粉刺(Shadow Acne): 这是一种错误的自阴影现象,表面会呈现出条纹状或噪点状的阴影 1。其根本原因是浮点数精度限制和阴影贴图的离散化。在进行阴影测试时,一个片元的计算出的光源空间深度值,可能由于微小的浮点误差,恰好比存储在阴影贴图中代表同一表面的深度值稍大一点。这会导致该片元错误地判定自己被自己遮挡,从而产生不该有的阴影 1
    • 彼得潘现象(Peter Panning): 这是阴影粉刺的“反面”。为了解决阴影粉刺,我们通常会引入一个偏移量(bias)。如果这个偏移量设置得过大,阴影就会看起来与投射它的物体分离,使物体像是“飘”在空中,如同彼得·潘的影子一样 8
  • 偏移(Biasing)的艺术(DirectX 12 实现): 为了在阴影粉刺和彼得潘现象之间找到平衡,需要采用精细的偏移策略。
    • 常量深度偏移(Constant Depth Bias):最简单的修复方法。在深度比较时,为当前片元的深度加上一个固定的微小偏移量(例如 currentDepth > closestDepth - bias),或者在生成阴影贴图时就施加偏移 1。在DirectX 12中,这可以通过 D3D12_RASTERIZER_DESC结构体中的DepthBias成员来控制。
    • 斜率缩放深度偏移(Slope-Scaled Depth Bias):一种更健壮的解决方案。偏移量的大小会根据多边形相对于光源的斜率进行缩放。与光线方向接近平行的多边形(即斜率大)更容易产生阴影粉刺,因此会获得一个更大的偏移量;而正对光源的多边形(斜率小)则获得较小的偏移 9。这是现代GPU的一项硬件特性,在DX12中通过 D3D12_RASTERIZER_DESC的SlopeScaledDepthBias成员控制。这是目前缓解阴影粉刺而不引入过多彼得潘现象的标准推荐方法 9

1.3. 优化光源视图:紧凑视锥体拟合

为了最大化利用阴影贴图的有限分辨率,光源的投影矩阵应该被计算得尽可能紧密地包围住那些对摄像机可见且能投射阴影的物体 8。一个松散、任意的视锥体会将宝贵的纹素浪费在空旷区域,从而加剧走样问题 9

实现步骤(以平行光为例)

  1. 获取摄像机视锥体在世界空间中的8个角点。
  2. 将这8个角点变换到光源的视图空间(使用一个根据光源方向构建的视图矩阵)。
  3. 在光源的视图空间中,找到这8个变换后角点的最小和最大的X、Y坐标。这些值定义了光源正交投影矩阵的left、right、top和bottom平面。
  4. 同样,找到最小和最大的Z坐标。这些值定义了near和far平面。使用尽可能紧凑的近/远平面对于保证深度缓冲区的精度至关重要 9

一个重要的考量是稳定性与紧凑性的权衡。虽然每一帧都计算最紧凑的视锥体在分辨率利用上是最优的,但这会导致当摄像机移动时,阴影贴图的纹素与世界空间位置的对应关系不断变化,从而引起阴影边缘的“闪烁”或“蠕动”。一个关键的实现技巧是稳定化投影,通过将光源视锥体的边界舍入到与阴影贴图纹素大小相对应的世界空间离散步长上,来确保帧间的稳定性 10。这是实现高质量、稳定阴影效果的必要步骤。

1.4. 针对特定光源的实现

  • 平行光(Directional Light)与聚光灯(Spot Light): 平行光(如太阳光)使用正交投影(Orthographic Projection)6。聚光灯则使用透视投影(Perspective Projection),其光照锥体自然地定义了它自己的视锥体 11
  • 点光源(Point Light)与全向阴影贴图(Omnidirectional Shadow Mapping)
    • 原理:点光源向所有方向发射光线。为了捕捉这一点,我们必须将场景渲染六次,分别对应一个立方体贴图(Cubemap)的六个面(+X, -X, +Y, -Y, +Z, -Z)。每次渲染都使用一个90度视角的透视投影,朝向对应的面 2
    • 性能成本:这种方法的开销极高,相当于为六个独立的聚光灯渲染阴影,显著增加了绘制调用(Draw Call)和几何处理的数量 12
    • DirectX 12 实现:这涉及到创建一个D3D12_RESOURCE_DESC来描述一个立方体贴图资源(D3D12_RESOURCE_DIMENSION_TEXTURE2D且ArraySize = 6)。在阴影生成遍中,通过一个循环迭代六次,每次更新光源的视图矩阵以朝向不同的面,并通过一个指向特定数组切片的渲染目标视图(RTV)将结果渲染到对应的立方体贴图面。
    • 几何着色器(Geometry Shader)优化:一种更高级的DX12技术可以利用几何着色器在单遍渲染中完成对所有六个面的绘制。几何着色器接收输入的三角形,用六个不同的视图-投影矩阵对其进行变换,并通过设置SV_RenderTargetArrayIndex语义,将图元输出到对应的立方体贴图层 13。这减少了CPU端的开销,但增加了GPU在几何着色器阶段的负载。
    • 替代方案 - 双抛物面阴影贴图(Dual Paraboloid Shadow Mapping):一种不太常见但更快的替代方案,它使用两个抛物面投影在两遍渲染中捕捉完整的360度视图,而不是六遍。但这种方法可能会引入难以校正的扭曲瑕疵 14

DirectX 12的底层架构在一定程度上改变了性能的考量。虽然GPU端的算法相同,但DX12通过命令列表(Command List)和管线状态对象(PSO)等机制,极大地降低了CPU在发出多次绘制调用时的开销 15。这意味着,对于像全向阴影贴图这样的多遍算法,CPU端的瓶颈相较于DX11有所缓解。性能的瓶颈更加纯粹地转移到了GPU端,即执行六次场景渲染的原始计算成本。因此,在DX12环境下,动态点光源阴影在某些场景中可能比以往更具可行性,优化的重点也随之转向GPU端,例如更积极地对每个阴影遍进行物体剔除 14

第二部分:行业主力——级联阴影贴图(Cascaded Shadow Maps)

级联阴影贴图(Cascaded Shadow Maps, CSM)是现代游戏引擎中处理大规模平行光(如太阳光)阴影的标准解决方案 3。它直接针对标准阴影贴图在宏大场景中面临的分辨率困境。

2.1. 原理:解决分辨率困境

  • 问题所在:对于一个广阔的开放世界场景,单一的平行光阴影贴图必须覆盖摄像机能看到的所有区域。这导致其有限的纹素(texel)被过度拉伸,使得近处的物体阴影分辨率极低,呈现出模糊、锯齿状或完全失真的效果 8
  • 解决方案:将摄像机的视锥体沿其深度轴(Z轴)分割成多个切片,这些切片被称为“级联”(Cascades)8。为每个级联渲染一张独立的、高分辨率的阴影贴图。离摄像机最近的级联覆盖一个较小的空间范围,因此其阴影贴图具有极高的细节密度。随后的级联依次覆盖越来越大的空间范围,其细节密度相应降低 3。这种策略将纹素密度智能地分配到了最需要它的地方——玩家的眼前。

2.2. DirectX 12 实现深度解析

  • 视锥体分割
    • 分割方案:视锥体沿视图空间的Z轴进行分割。常见的分割方案包括:线性分割(每级联覆盖相同的深度范围)、对数分割(级联在靠近摄像机处更密集、更窄),以及实用混合分割(通常是前两者的结合,例如第一级非常靠近摄像机,其余级联按对数或线性分布)16。为了防止阴影边缘在摄像机移动时发生闪烁,通常会为特定场景采用一组静态的级联分割距离 16
    • DX12实现:这是一个在CPU端完成的计算。每一帧,你需要计算出每个级联子视锥体在世界空间中的8个角点。
  • 资源管理(纹理数组 vs. 纹理图集)
    • 纹理数组(Texture Array):这是现代且首选的方法。通过创建一个ID3D12Resource,并将其ArraySize设置为级联的数量,可以得到一个纹理数组。这在DX12中通过D3D12_TEX2D_ARRAY_SRV来访问。这种方式既高效,又便于在着色器中通过索引访问特定级联的阴影贴图 8
    • 纹理图集(Texture Atlas):一种较老的方法,将所有级联的阴影贴图渲染到一张巨大的纹理的不同视口(viewport)上。这在旧的硬件或API(如DX10.0)上是必需的,因为它们不支持对纹理数组进行某些过滤操作(如PCF)16。在纯DX12引擎中,纹理数组是更优越的选择。
  • 投影计算与稳定化
    • 对于每个级联,根据其子视锥体的角点计算一个紧凑的正交投影矩阵,如1.3节所述 8
    • 稳定化至关重要:为防止摄像机移动或旋转时阴影边缘发生“蠕动”或“抖动”的瑕疵,必须对光源的正交投影进行稳定化处理。这通过将计算出的视锥体包围盒的最小/最大边界,舍入到最接近的世界空间纹素大小的倍数来实现。这确保了阴影贴图的纹素在世界空间中是对齐的,从而消除了时间上的不稳定性 10
// 稳定化伪代码
float cascadeFrustumSize = max(bounds.max.x - bounds.min.x, bounds.max.y - bounds.min.y);
float worldUnitsPerTexel = cascadeFrustumSize / shadowMapResolution;

// 将包围盒中心移动到纹素网格上
float2 center = (bounds.min.xy + bounds.max.xy) * 0.5f;
center.x = floor(center.x / worldUnitsPerTexel) * worldUnitsPerTexel;
center.y = floor(center.y / worldUnitsPerTexel) * worldUnitsPerTexel;

// 从稳定化的中心重新计算包围盒
float2 halfSize = (bounds.max.xy - bounds.min.xy) * 0.5f;
bounds.min.xy = center - halfSize;
bounds.max.xy = center + halfSize;

2.3. 着色与级联选择

  • 向GPU传递数据:每个级联的光源视图-投影矩阵,以及级联的分割距离(通常在视图空间Z轴上定义),被打包到常量缓冲区(Constant Buffer)中并发送给着色器。

  • 在像素着色器中选择级联:着色器必须确定当前片元属于哪个级联。

    1. 基于区间的选择:比较片元的视图空间Z深度与预定义的级联分割距离。这可以是一系列简单的if语句,也可以通过向量化操作实现 16。这种方法通常速度更快。
    2. 基于图的选择:将片元的位置变换到每个级联的UV空间,并检查其坐标是否在``范围内。第一个匹配的即是正确的级联。这种方法可以更有效地利用分辨率,但计算上更复杂 16
  • HLSL实现草图(基于区间)

// 从CPU传入的常量
cbuffer CSMConstants
{
    float4 g_CascadeSplits; // 视图空间Z深度,定义了级联的边界
    matrix g_LightViewProj;
};

Texture2DArray g_CascadeShadowMap : register(t0);
SamplerComparisonState g_ShadowSampler : register(s0); // 使用比较采样器

// 在像素着色器中
float viewDepth = input.ViewPosition.z;

// 选择级联索引
int cascadeIndex = 0;
// 使用点积进行无分支选择
float4 comparisons = (viewDepth > g_CascadeSplits);
cascadeIndex = dot(comparisons, float4(1, 1, 1, 1));

// 变换到正确的级联的光源空间
float4 shadowCoord = mul(g_LightViewProj[cascadeIndex], input.WorldPosition);

// 使用 SampleCmpLevelZero 进行深度比较
// shadowCoord.z / shadowCoord.w 是当前深度
// float3(shadowCoord.xy / shadowCoord.w, cascadeIndex) 是采样坐标
float shadowFactor = g_CascadeShadowMap.SampleCmpLevelZero(
    g_ShadowSampler, 
    float3(shadowCoord.xy / shadowCoord.w, float(cascadeIndex)), 
    shadowCoord.z / shadowCoord.w
);
  • 级联间混合:为了隐藏不同级联交界处的明显接缝(由分辨率突变引起),可以定义一个小的混合区域。在此区域内,像素会同时从两个相邻的级联中采样,并根据其在混合带中的位置对结果进行线性插值。这需要在着色器中进行仔细的分支处理,以避免对不在混合区域内的像素造成性能损失 16

2.4. 性能剖析

  • CPU成本:中等。每帧都需要计算视锥体分割和每个级联的矩阵。
  • GPU成本:高。主要成本来自于多次渲染场景几何体(每个级联一次)。这显著增加了绘制调用次数和顶点处理负载。成本与级联数量和每个级联中的几何体数量成正比。
  • 内存/带宽:高。需要存储多个阴影贴图,增加了显存(VRAM)占用和读写这些贴图所需的带宽。

由于GPU成本高昂,积极的剔除策略对于CSM的性能至关重要。对于每个级联的阴影生成遍,必须剔除那些既不在该级联视锥体内,也不在光源视锥体内的物体.14 这是一个不容忽视的优化,是使CSM性能可控的关键。

CSM本身并非单一固定的算法,而是一个可配置的框架。分割方案、稳定化方法、级联选择逻辑和混合技术的选择,都对最终的视觉质量和性能有巨大影响。开发者在实现时,应将其视为一个解决系列问题的过程:问题1(分辨率)-> 解决方案(级联);问题2(闪烁)-> 解决方案(投影稳定化);问题3(接缝)-> 解决方案(混合)。这种结构化的思维方式更有助于构建一个鲁棒的系统。

值得注意的是,业界最前沿的技术,如虚幻引擎的虚拟阴影贴图(Virtual Shadow Maps, VSM——注意,此VSM非方差阴影贴图),正是CSM思想的逻辑延伸和极致发展 4。虚拟阴影贴图使用一个极高分辨率的虚拟纹理(例如16k x 16k),并将其划分为许多小的图块(Pages)。这些图块根据屏幕上可见的内容按需分配和渲染 17。这相当于将4-8个大型级联替换为成千上万个被动态管理的微型级联。虽然完整实现一套虚拟阴影贴图系统对于多数团队而言过于复杂,但理解其原理,可以将CSM视为实现这一宏伟目标的实用基础。

第三部分:追求柔和——高级过滤与技术演进

在现实世界中,很少有阴影是完美锐利的。本节将详细介绍从简单的过滤技术到基于物理近似的方法,以在实时渲染中实现柔和、自然的阴影效果。

3.1. 百分比渐近过滤(Percentage-Closer Filtering, PCF)

  • 原理:为了软化阴影贴图的走样边缘,PCF不再只进行一次深度采样和比较。而是在当前片元位置周围的一个核心(kernel)区域内进行多次采样。最终的阴影值是所有这些独立深度测试结果的平均值,即“更近的”样本所占的“百分比”3
  • 实现
    • 核心形状:一个简单的方形(box)核心会产生块状的瑕疵。更好的方法是使用非规则的采样模式,如**泊松盘(Poisson disk)沃格尔盘(Vogel disk)**分布。这些模式的采样点分布不均,可以有效打散规律性。再结合逐像素的随机旋转,可以进一步消除重复图案,产生更自然的噪点状半影 3
    • DirectX 12 硬件支持:现代GPU硬件内置了对2x2双线性PCF的支持。在DX12中,这通过带有D3D12_FILTER_COMPARISON_*标志的采样器状态(D3D12_SAMPLER_DESC)和HLSL中的SampleCmpLevelZero函数来暴露 18。此函数接收一个比较值,执行四次深度测试,并返回一个经过双线性插值的0到1之间的结果。要构建更大的过滤器(例如一个3x3的9-tap过滤器),需要在循环中多次调用 SampleCmpLevelZero,每次使用不同的纹理坐标偏移 18
  • 性能:成本与纹理采样次数成正比。一个16-tap的PCF过滤器在纹理采样方面的开销是硬阴影测试的16倍。这很容易成为内存带宽的瓶颈 7

3.2. 可过滤格式:方差阴影贴图(Variance Shadow Maps, VSM)

PCF的性能瓶颈在于其“先比较,后过滤”的模式,无法利用GPU强大的硬件纹理过滤能力。VSM的出现正是为了解决这个问题。

  • 原理:VSM的核心思想是改变阴影贴图中存储的数据。它不再存储单一的深度值,而是存储一个过滤区域内深度分布的统计信息。具体来说,它在一个双通道纹理中存储深度的一阶矩(均值,$E(x)$)和二阶矩(平方的均值,$E(x^2)$)7。由于矩是线性可加的,因此这种格式的阴影贴图可以像普通颜色纹理一样,直接使用硬件的双线性/三线性/各向异性过滤、Mipmap等 7。在着色阶段,通过这两个矩计算出方差( $Variance = E(x^2) - E(x)^2$),然后利用**切比雪夫不等式(Chebyshev’s inequality)**来估算一个点被遮挡的概率上限,从而得到阴影强度 19
  • 漏光(Light Bleeding)问题
    • 成因:这是VSM最致命的缺陷。当多个遮挡物在光源视角下重叠,且它们的深度相差很大时,对应阴影贴图区域的方差会变得非常大。对于高方差的分布,切比雪夫不等式给出的概率上界非常松散,导致一个本应完全处于阴影中的区域(可见性为0)被计算出一个非零的可见性值。这在视觉上表现为光线“穿透”了遮挡物,即“漏光”19
    • 缓解措施
      • 漏光抑制因子:一种常见的“hack”手段,通过一个可调参数来重映射切比雪夫测试的输出范围,将较小的可见性值强行压制为0。这在着色器中通常通过一个linstep或smoothstep函数实现 20。它能有效减少漏光,但代价是可能过度加深阴影,损失半影区域的细节。
      • 分层方差阴影贴图(Layered VSM, LVSM):将光源的深度范围分割成多个层。通过减小每一层内的深度跨度,可以有效降低方差,从而抑制漏光。这种方法效果显著,但增加了存储和实现的复杂度 21
      • 指数方差阴影贴图(Exponential VSM, EVSM):在存储矩之前,对深度值进行指数变换。这种非线性变换能更有效地分配精度,极大地减少漏光问题。但它通常需要高精度的纹理格式(如32位浮点),并且对指数参数的选择较为敏感 22
  • 性能:主要开销在于需要更大的纹理格式(例如R32G32_FLOAT)以及可选的后期高斯模糊处理。然而,最终的着色阶段非常廉价(只需一次硬件过滤的纹理查找)。对于需要非常柔和的大范围半影的场景,VSM可能比高采样数的PCF过滤器更快 23

VSM与PCF之间的根本权衡在于:PCF在数学上是“正确”的(它平均的是布尔测试结果),但无法被硬件直接过滤;VSM是可被硬件过滤的,但其测试过程是“近似”的,从而导致了漏光等瑕疵。开发者需要理解这个性能与质量的交易:VSM适用于追求廉价、大范围模糊的场景,并愿意接受其正确性上的妥协;而PCF则适用于追求正确、小范围模糊的场景,并愿意为此支付多重采样的代价。

3.3. 基于物理的柔和度:百分比渐近柔和阴影(PCSS)

  • 原理:PCSS旨在模拟物理上更准确的半影(penumbra)。在现实中,阴影在靠近遮挡物与接收面接触点时非常锐利(接触硬化,contact hardening),随着遮挡物与接收面距离的增加,阴影会变得越来越柔和、模糊 3
  • 算法分解(三步法)
    1. 遮挡物搜索(Blocker Search):对于当前着色的像素,在其对应的阴影贴图区域进行采样,以找到实际遮挡光线的物体(blockers)的平均深度。这个搜索区域的大小与光源的物理尺寸成正比 24
    2. 半影估算(Penumbra Estimation):利用上一步得到的平均遮挡物深度、当前像素(接收面,receiver)的深度以及光源的物理尺寸,通过一个简单的相似三角形公式来估算半影的宽度:$PenumbraWidth = (ReceiverDepth - BlockerDepth) * LightSize / BlockerDepth$ 24。这个公式是PCSS的核心,它将物理关系(距离、光源大小)转化为了一个可计算的模糊半径。
    3. 可变宽度PCF(Variable-Width PCF):最后,执行一次标准的PCF过滤(如3.1节所述),但PCF核心的半径(即模糊程度)是由第二步计算出的PenumbraWidth决定的 24
  • 实现与性能:PCSS的计算成本相当高昂。它需要两轮多重采样操作(遮挡物搜索和最终的PCF),使其比标准PCF或VSM慢得多 24。每一步的采样数量是质量与性能之间的关键权衡参数 25。在实际应用中,PCSS通常构建在CSM系统之上,用于处理平行光的阴影。

尽管昂贵,PCSS提供了光栅化方法中视觉效果最可信的柔和阴影。它正确地模拟了接触硬化这一重要的感知线索。对于一个追求顶级画质的引擎来说,PCSS是光栅化阴影的“终极”质量选项。

第四部分:新纪元——光线追踪阴影

本节将探讨由专用硬件和现代API(如DirectX Raytracing, DXR)推动的范式转变——从基于光栅化的近似方法,转向更符合物理规律的模拟。

4.1. 范式转变:从深度图到光线相交

  • 根本差异:光线追踪不再依赖于从光源视角渲染深度图。它直接模拟光线的传播路径。要判断一个点是否在阴影中,只需从该点向光源发射一条“阴影光线”(shadow ray)。如果这条光线在到达光源前与任何物体相交,则该点处于阴影中;如果光线无阻碍地到达光源,则该点被照亮 26
  • 无可辩驳的质量优势
    • 几何精确性:这种方法在几何上是精确的。它从根本上消除了所有经典的阴影贴图瑕疵,如分辨率走样、阴影粉刺和彼得潘现象。不再需要任何形式的偏移(biasing)11。它自然地产生完美的硬边阴影。
    • 物理精确的软阴影:为了模拟面光源(area light)产生的软阴影,我们不再只发射一条阴影光线,而是向光源表面上的多个随机点发射多条光线。最终的阴影强度是被遮挡光线所占的百分比 26。这种蒙特卡洛方法自然而然地产生了接触硬化和物理正确的半影,无需PCSS那样复杂的多步近似过程 27

4.2. DirectX Raytracing (DXR) 实现指南

  • DXR核心组件

    • 加速结构(Acceleration Structures, AS):这是光线追踪性能的核心。它是一个两级层次结构。**底层加速结构(Bottom-Level AS, BLAS)**存储单个网格的几何信息。**顶层加速结构(Top-Level AS, TLAS)**存储BLAS的实例及其世界变换矩阵。GPU利用这个结构来快速查找光线与三角形的交点 28
    • 光线追踪管线状态对象(RTPSO):一种新型的PSO,它捆绑了所有需要的光线追踪着色器(光线生成、未命中、命中组)和管线配置(如最大递归深度)29
    • 着色器绑定表(Shader Binding Table, SBT):一块GPU内存,作用类似于一个函数指针查找表。它将几何体实例映射到它们在被光线击中时应该执行的具体命中着色器,从而允许不同材质对光线做出不同响应 29
  • 实现高效的遮挡光线

    • 光线负载(Ray Payload):对于一条简单的阴影光线,其负载可以是一个单一的布尔值(bool isOccluded)。这比传递颜色等复杂数据要高效得多 30
    • 光线生成着色器(Ray Generation Shader):这是由DispatchRays()命令调用的入口点。它通常从G-Buffer中读取表面点的世界位置,然后从该点向光源投射阴影光线 31
    • 命中着色器(Hit Shaders)
      • 最近命中着色器(Closest-Hit Shader):对于简单的非透明阴影,最近命中着色器可以非常简单。它唯一需要做的事情就是将负载中的isOccluded设置为true,然后就可以终止 30
      • 任意命中着色器(Any-Hit Shader):这是实现高效遮挡光线的关键。任意命中着色器在光线路径上每个潜在的交点都会被调用。对于不透明物体的阴影,可以在追踪光线时设置RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH标志。这个标志告诉硬件,只要找到第一个交点,就立即停止搜索并报告命中。这极大地加速了遍历过程,此时甚至不需要为不透明几何体提供任意命中着色器。任意命中着色器主要用于处理带有透明贴图的材质(alpha-testing),它可以在着色器中判断纹理的透明部分,并决定是否忽略当前交点,让光线继续前进 29
    • 未命中着色器(Miss Shader):如果阴影光线没有击中任何几何体,未命中着色器就会被调用。对于阴影光线而言,这意味着该点被照亮。未命中着色器不需要对负载做任何操作,使其保持默认的isOccluded = false状态 30
  • HLSL/DXR 代码草图

// 定义阴影光线的负载结构
struct ShadowPayload
{
    bool bIsOccluded;
};

// DXR 场景加速结构
RaytracingAccelerationStructure g_SceneBVH : register(t0);

// 光线生成着色器
[shader("raygeneration")]
void ShadowRayGen()
{
    //... 从 G-Buffer 获取表面世界位置 P 和法线 N...
    uint2 dispatchIdx = DispatchRaysIndex().xy;
    float3 P = GetWorldPositionFromGBuffer(dispatchIdx);
    float3 N = GetWorldNormalFromGBuffer(dispatchIdx);

    float3 lightDir = normalize(g_LightPosition - P);

    RayDesc ray;
    ray.Origin = P + N * 0.001f; // 施加偏移避免自相交
    ray.Direction = lightDir;
    ray.TMin = 0.0f;
    ray.TMax = length(g_LightPosition - P); // 光线长度限制在到光源的距离

    ShadowPayload payload;
    payload.bIsOccluded = false; // 默认未被遮挡

    // 追踪光线。使用标志来提高效率!
    // RayFlags: 接受首次命中并结束搜索
    // InstanceInclusionMask: 0xFF 表示与所有物体相交
    // MissShaderIndex: 在SBT中的未命中着色器索引
    // HitGroupIndex: 在SBT中的命中组索引偏移
    TraceRay(g_SceneBVH, 
             RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH, 
             0xFF, 
             1, /* Miss Shader Index */
             0, /* Multiplier for Hit Group Index */
             0, /* Hit Group Index Offset */
             ray, 
             payload);

    float shadowFactor = payload.bIsOccluded? 0.0f : 1.0f;

    //... 将 shadowFactor 应用于光照计算...
}

// 最近命中着色器
[shader("closesthit")]
void ShadowClosestHit(inout ShadowPayload payload, BuiltinIntersectionAttributes attribs)
{
    // 只要命中,就设置遮挡标志。
    // 由于使用了 RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH,
    // 这是光线遇到的第一个也是唯一一个命中。
    payload.bIsOccluded = true;
}

// 未命中着色器
[shader("miss")]
void ShadowMiss(inout ShadowPayload payload)
{
    // 如果光线未命中任何物体,它成功到达了光源。
    // 无需做任何事,payload.bIsOccluded 保持其默认的 false 值。
}

4.3. 性能剖析

  • CPU成本:主要在于为动态物体(如蒙皮角色)构建或更新TLAS。对于静态场景,这部分成本可以忽略不计。
  • GPU成本:成本由两个主要因素决定:
    1. BVH构建:每帧为动态几何体(如蒙皮网格)重新构建BLAS可能会非常昂贵 32
    2. 光线遍历:这是主要的渲染成本。在支持硬件光追的GPU上(如NVIDIA RTX系列),这项工作由专用的RT Core处理,远快于在通用着色器核心上执行 33。成本与投射的光线数量和场景遍历的复杂性成正比。
  • 内存/带宽:加速结构本身会消耗大量显存。一个复杂场景的TLAS/BLAS可能需要数百兆字节的空间 32

光栅化与光线追踪的性能对比高度依赖于具体的应用场景。对于单一的平行光,一个高度优化的CSM实现几乎肯定比在整个屏幕上追踪数十亿条光线要快。然而,对于一个包含数百个小型动态点光源的场景,为每个光源生成和渲染阴影立方体贴图的成本是天文数字。在这种情况下,为每个像素向每个光源追踪一条简单的遮挡光线,在具备硬件光追能力的GPU上反而可能更快 34。这是一个必须理解的关键差异。因此,最佳策略往往是采用混合渲染:使用CSM处理太阳光,同时对局部光源有选择地使用光线追踪阴影。

第五部分:综合分析与战略建议

在详细剖析了各种主流阴影技术后,本节将进行横向对比,并为您的引擎提供一套分阶段、可扩展的实现路线图。

5.1. 对比分析

为了清晰地展现各种技术的权衡,我们提供以下两个表格。

表1:阴影技术定性特征对比

技术阴影类型柔和度质量接触硬化关键瑕疵主要应用场景
标准SM硬阴影不适用不适用走样、阴影粉刺、彼得潘简单场景、教学、小型聚光灯
CSM硬阴影不适用不适用级联间接缝、闪烁(若未稳定)开放世界中的平行光(太阳)
PCF软阴影均匀模糊核心采样图案、性能开销通用的柔和阴影基础技术
VSM软阴影均匀模糊漏光、精度问题需要大范围、廉价模糊的场景
PCSS软阴影物理可变性能开销高、实现复杂高质量的平行光/聚光灯软阴影
光线追踪硬/软阴影物理精确性能开销、需要专用硬件高保真渲染、多光源场景、混合渲染

表2:阴影技术量化性能剖析

技术CPU成本 (设置/绘制)GPU成本 (几何/填充率)GPU成本 (ALU)GPU成本 (纹理/遍历)显存占用DX12优势
标准SM中 (1遍几何)低 (1次采样)低CPU开销
CSM (N遍几何)低 (1次采样/级联)低CPU开销
PCF (N遍几何)极高 (多重采样)低CPU开销
VSM (N遍几何)中 (模糊/统计)低 (1次过滤采样)中-高 (高精度格式)低CPU开销
PCSS (N遍几何)高 (搜索/估算)极高 (多重采样x2)低CPU开销
光线追踪中 (BVH构建)高 (着色器逻辑) (光线遍历) (BVH存储)原生API支持

5.2. 为您的引擎推荐的实现路线图

基于上述分析,我们不推荐寻找一个“万能”的阴影算法,而是建议构建一个分层的、可扩展的混合阴影系统。

1. 奠定基石(必需):CSM + PCF

  • 实现用于平行光(太阳)的级联阴影贴图(CSM)。这是处理开放世界阴影无可争议的行业标准 35。首要任务是确保投影计算的 紧凑性稳定性
  • 实现一个可配置的百分比渐近过滤器(PCF)。使用DirectX 12的SampleCmpLevelZero硬件支持作为基础 18。采用 旋转泊松盘采样模式,以在不过度增加采样数的情况下获得高质量的柔和边缘 3。这将是您引擎的柔和阴影基线。

2. 迈向高画质(可选):PCSS

  • 在您的CSM系统之上,将百分比渐近柔和阴影(PCSS)作为一个高端图形选项来实现。这将为您的引擎带来物理上可信的柔和阴影和接触硬化效果,这是光栅化方法的画质巅峰 24

3. 局部光源策略(混合方案)

  • 聚光灯:使用标准的、非级联的阴影贴图,并应用PCF或PCSS进行软化。
  • 点光源:实现全向阴影贴图(Cubemaps)。要充分意识到其高昂的性能成本 12
  • 战略性建议:为局部光源(聚光灯和点光源)提供光线追踪阴影作为备选方案。在包含大量局部光源的场景中,光线追踪的性能可能优于生成数十张阴影贴图的传统方法。这是混合渲染范式的完美应用场景。

4. 通往次世代(DXR)

  • DXR阴影定位为“超级”画质设置。从最简单的实现开始:使用高效的遮挡光线为局部光源生成完美的硬阴影。
  • 在此基础上,通过向面光源表面投射多条光线来扩展支持软阴影。
  • 请注意,对于多重采样的光线追踪效果,一个高质量的**降噪器(Denoiser)**是必不可少的,但这本身就是一个庞大而复杂的主题,需要单独研究 36

5. 初期应避免的技术

  • 阴影体积(Shadow Volumes):尽管在历史上很重要(例如在《DOOM 3》中),但在现代硬件上,由于其极高的填充率要求和对封闭几何体的严格限制,其性能通常不如阴影贴图 14
  • 方差阴影贴图(VSM):虽然它提供了廉价且宽广的模糊效果,但其固有的漏光瑕疵在各种复杂场景中都难以稳健地处理 20。对于一个通用引擎,PCF/PCSS的可靠性是更佳的选择。VSM更适合作为一种特定风格或场景下的专用技术。

最终行动路径总结

  1. 第一步:为平行光构建一个稳定、优化、带稳定化处理的CSM系统。
  2. 第二步:集成一个高质量、可配置的PCF过滤器。
  3. 第三步:在CSM之上叠加PCSS,作为“高画质”选项。
  4. 第四步:开始DXR集成,首先将其应用于局部光源,拥抱混合渲染的未来。

遵循此路线图,您将能够为您的DirectX 12引擎构建一个功能强大、视觉出色且具备未来扩展性的现代实时阴影系统。

Works cited


  1. Shadow Mapping - LearnOpenGL, accessed June 16, 2025, https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. Chapter 12. Omnidirectional Shadow Mapping - NVIDIA Developer, accessed June 16, 2025, https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-12-omnidirectional-shadow-mapping ↩︎ ↩︎

  3. Dynamic Shadows - Real-Time Techs #2 - Hypesio, accessed June 16, 2025, https://hypesio.fr/en/dynamic-shadows-real-time-techs/ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  4. Shadow algorithms for real-time rendering - Michael Schwarz, accessed June 16, 2025, https://michael-schwarz.com/research/publ/files/shadowcourse-eg10.pdf ↩︎

  5. Real-time Shadows - MIT OpenCourseWare, accessed June 16, 2025, https://ocw.mit.edu/courses/6-837-computer-graphics-fall-2012/9c1872964d555ec0ae5006a63d7636b4_MIT6_837F12_Lec23.pdf ↩︎

  6. Tutorial 16 : Shadow mapping, accessed June 16, 2025, http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/ ↩︎ ↩︎

  7. Chapter 8. Summed-Area Variance Shadow Maps | NVIDIA Developer, accessed June 16, 2025, https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-8-summed-area-variance-shadow-maps ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  8. CSM - LearnOpenGL, accessed June 16, 2025, https://learnopengl.com/Guest-Articles/2021/CSM ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  9. Common Techniques to Improve Shadow Depth Maps - Win32 apps …, accessed June 16, 2025, https://learn.microsoft.com/en-us/windows/win32/dxtecharts/common-techniques-to-improve-shadow-depth-maps ↩︎ ↩︎ ↩︎ ↩︎

  10. Cascade Shadow Map Problems DirectX 11 - Game Development Stack Exchange, accessed June 16, 2025, https://gamedev.stackexchange.com/questions/131486/cascade-shadow-map-problems-directx-11 ↩︎ ↩︎

  11. Shadow mapping - Wikipedia, accessed June 16, 2025, https://en.wikipedia.org/wiki/Shadow_mapping ↩︎ ↩︎

  12. Shadow mapping - Unity - Manual, accessed June 16, 2025, https://docs.unity3d.com/6000.1/Documentation/Manual/shadow-mapping.html ↩︎ ↩︎

  13. Point Shadows - LearnOpenGL, accessed June 16, 2025, https://learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows ↩︎

  14. How do modern game engines handle many shadow casting lights?, accessed June 16, 2025, https://gamedev.stackexchange.com/questions/139545/how-do-modern-game-engines-handle-many-shadow-casting-lights ↩︎ ↩︎ ↩︎ ↩︎

  15. DirectX 12 - Microsoft Developer Blogs, accessed June 16, 2025, https://devblogs.microsoft.com/directx/directx-12/ ↩︎

  16. Cascaded Shadow Maps - Win32 apps | Microsoft Learn, accessed June 16, 2025, https://learn.microsoft.com/en-us/windows/win32/dxtecharts/cascaded-shadow-maps ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  17. Virtual Shadow Maps in Unreal Engine | Unreal Engine 5.6 …, accessed June 16, 2025, https://dev.epicgames.com/documentation/en-us/unreal-engine/virtual-shadow-maps-in-unreal-engine ↩︎

  18. questions about shadow map PCF - directx 12 - Stack Overflow, accessed June 16, 2025, https://stackoverflow.com/questions/74148851/questions-about-shadow-map-pcf ↩︎ ↩︎ ↩︎

  19. Variance Shadow Maps, accessed June 16, 2025, https://pierremezieres.github.io/site-co-master/references/vsm_paper.pdf ↩︎ ↩︎

  20. Variance Shadow Maps (VSM) · Delt06/toon-rp Wiki - GitHub, accessed June 16, 2025, https://github.com/Delt06/toon-rp/wiki/Variance-Shadow-Maps-(VSM ↩︎ ↩︎

  21. Layered Variance Shadow Maps - CiteSeerX, accessed June 16, 2025, https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=a021694be34926fdcafb52495906eabff35297ca ↩︎

  22. Opinions on VSM and derivatives : r/GraphicsProgramming - Reddit, accessed June 16, 2025, https://www.reddit.com/r/GraphicsProgramming/comments/1aknzx0/opinions-on-vsm-and-derivatives/ ↩︎

  23. Variance Shadow Maps Light-Bleeding Reduction Tricks | 21 | GPU Pro 2 - Taylor & Francis eBooks, accessed June 16, 2025, https://www.taylorfrancis.com/chapters/edit/10.1201/b11325-21/variance-shadow-maps-light-bleeding-reduction-tricks-wojciech-sterna ↩︎

  24. Percentage-Closer Soft Shadows | NVIDIA, accessed June 16, 2025, https://developer.download.nvidia.com/shaderlibrary/docs/shadow_PCSS.pdf ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  25. Percentage-Closer Soft Shadows - NVIDIA, accessed June 16, 2025, https://http.download.nvidia.com/developer/presentations/2005/SIGGRAPH/Percentage_Closer_Soft_Shadows.pdf ↩︎

  26. Ray tracing for beginners - Embedded Computing Design, accessed June 16, 2025, https://embeddedcomputing.com/technology/software-and-os/ray-tracing-for-beginners ↩︎ ↩︎

  27. Hardware Ray Tracing in Unreal Engine | Unreal Engine 5.6 …, accessed June 16, 2025, https://dev.epicgames.com/documentation/en-us/unreal-engine/hardware-ray-tracing-in-unreal-engine ↩︎

  28. Ray Tracing - NVIDIA Developer, accessed June 16, 2025, https://developer.nvidia.com/discover/ray-tracing ↩︎

  29. DX12 Raytracing tutorial - Part 2 - NVIDIA Developer, accessed June 16, 2025, https://developer.nvidia.com/rtx/raytracing/dxr/dx12-raytracing-tutorial-part-2 ↩︎ ↩︎ ↩︎

  30. DX12 Raytracing Tutorial - Extras - Another Ray Type | NVIDIA …, accessed June 16, 2025, https://developer.nvidia.com/rtx/raytracing/dxr/dx12-raytracing-tutorial/extra/dxr_tutorial_extra_another_ray_type ↩︎ ↩︎ ↩︎

  31. DirectX Raytracing (DXR) Functional Spec - Microsoft Open Source, accessed June 16, 2025, https://microsoft.github.io/DirectX-Specs/d3d/Raytracing.html ↩︎

  32. Ray Tracing Performance Guide in Unreal Engine - Epic Games Developers, accessed June 16, 2025, https://dev.epicgames.com/documentation/en-us/unreal-engine/ray-tracing-performance-guide-in-unreal-engine ↩︎ ↩︎

  33. Types of Ray Tracing, Performance On GeForce GPUs, and More - NVIDIA, accessed June 16, 2025, https://www.nvidia.com/en-us/geforce/news/gfecnt/geforce-gtx-dxr-ray-tracing-available-now/ ↩︎

  34. GPU Ray Tracing Performance Comparisons [2021-2022] | Page 92 - Beyond3D Forum, accessed June 16, 2025, https://forum.beyond3d.com/threads/gpu-ray-tracing-performance-comparisons-2021-2022.62346/page-92 ↩︎

  35. hypesio.fr, accessed June 16, 2025, https://hypesio.fr/en/dynamic-shadows-real-time-techs/#:~:text=The%20technique%20most%20commonly%20used,Engine%20and%20Unreal%20Engine%204)↩︎

  36. Hardware Ray Tracing Tips and Tricks in Unreal Engine - Epic Games Developers, accessed June 16, 2025, https://dev.epicgames.com/documentation/en-us/unreal-engine/hardware-ray-tracing-tips-and-tricks-in-unreal-engine ↩︎