WebGPU 阴影映射:从光源视角渲染深度图

原作者:webgpufundamentals Contributors
原始链接:https://webgpufundamentals.org/webgpu/lessons/webgpu-shadows.html

说明:您提供的原文片段仅包含页面导航、样式和侧边栏,<div className="lesson-main"> 中的实际正文内容缺失。因此以下内容不是逐句翻译,而是基于该教程公开版本的核心知识整理出的深度解读,并保留典型核心代码(简化示例,非原文逐字代码)。


一、阴影映射核心原理

阴影映射(Shadow Mapping)是实时渲染中最常用的阴影技术。其基本流程为:

  1. 从光源视角渲染场景:将场景深度写入一张深度纹理(Depth Texture)。
  2. 主渲染阶段比较深度:将片段变换到光源的投影空间,采样深度纹理,比较片段的实际深度与存储深度,判断该片段是否处于阴影中。

WebGPU 实现阴影映射时,需要创建一个仅包含深度附件的渲染通道,然后主渲染中使用深度比较采样器进行采样。


二、核心代码示例

1. 创建深度纹理

const depthTexture = device.createTexture({
  size: [1024, 1024],                 // 阴影贴图分辨率
  usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
  format: 'depth32float',             // 深度格式
});

const shadowSampler = device.createSampler({
  compare: 'less',                    // 深度比较函数
  magFilter: 'linear',
  minFilter: 'linear',
});

2. 渲染深度通道(光源视角)

const depthEncoder = device.createCommandEncoder();
const depthPass = depthEncoder.beginRenderPass({
  colorAttachments: [],               // 没有颜色附件
  depthStencilAttachment: {
    view: depthTexture.createView(),
    depthClearValue: 1.0,             // 远平面深度为 1.0
    depthLoadOp: 'clear',
    depthStoreOp: 'store',
  },
});
depthPass.setPipeline(depthPipeline);
// ... 绑定光源视角的 VP 矩阵并绘制场景 ...
depthPass.end();

3. 主渲染着色器(WGSL)

struct Uniforms {
  cameraViewProjectionMatrix: mat4x4f,
  lightViewProjectionMatrix: mat4x4f,
  lightDirection: vec3f,
};

@group(0) @binding(0) `var<uniform>` uni: Uniforms;
@group(1) @binding(0) var shadowMap: texture_depth_2d;
@group(1) @binding(1) var shadowSampler: sampler_comparison;

struct FragmentInput {
  @builtin(position) position: vec4f,
  @location(0) normal: vec3f,
  @location(1) shadowPosition: vec4f,
};

@vertex
fn vsMain(@location(0) position: vec3f,
          @location(1) normal: vec3f) -> FragmentInput {
  var out: FragmentInput;
  out.position = uni.cameraViewProjectionMatrix * vec4f(position, 1.0);
  out.normal = normal;
  out.shadowPosition = uni.lightViewProjectionMatrix * vec4f(position, 1.0);
  return out;
}

@fragment
fn fsMain(frag: FragmentInput) -> @location(0) vec4f {
  // 将阴影坐标从裁剪空间转换到 NDC,再映射到 [0,1] 纹理坐标
  let shadowPos = frag.shadowPosition.xyz / frag.shadowPosition.w;
  let uv = shadowPos.xy * 0.5 + 0.5;

  // 深度比较采样,返回 0 或 1
  let inShadow = textureSampleCompare(
    shadowMap,
    shadowSampler,
    uv,
    shadowPos.z - 0.0005   // bias 防止阴影痤疮
  );

  let lightAngleDot = dot(normalize(uni.lightDirection), normalize(frag.normal));
  let light = saturate(lightAngleDot);
  let shadowFactor = select(0.0, 1.0, inShadow);

  let baseColor = vec3f(0.8, 0.8, 0.8);
  return vec4f(baseColor * light * shadowFactor, 1.0);
}

三、三个核心优化要点

  1. 使用硬件深度比较采样器(sampler_comparison + textureSampleCompare)
    WebGPU 允许创建带 compare 参数的采样器,配合 texture_depth_2d 纹理直接进行深度比较。这不仅能减少手动比较的代码量,还能触发硬件加速的 PCF(Percentage Closer Filtering),在采样的同时完成比较与线性过滤,大幅降低阴影边缘锯齿。

  2. 应用深度偏移(Depth Bias)
    由于深度纹理的分辨率有限,直接比较深度值容易产生阴影痤疮(shadow acne)。在比较时减去一个微小偏移量 bias(如 0.0005)可以显著减轻该问题。偏移量需根据场景尺度调整,过大会导致“阴影悬浮”(peter panning)。

  3. 优化阴影贴图渲染路径

    • 仅渲染深度:阴影 pass 中不绑定颜色附件,只写深度,节省带宽。
    • 使用紧凑投影矩阵:光源视锥应尽可能紧贴场景包围盒,避免深度范围浪费。
    • 合理选择分辨率:阴影贴图分辨率直接影响阴影质量与性能,需根据光源影响范围权衡。
    • 重用阴影贴图:如果光照方向固定,可只更新需要移动的对象。

四、GLSL/WebGL ↔ WGSL/WebGPU 术语检查

GLSL / WebGL 术语WGSL / WebGPU 对应说明
sampler2DShadowtexture_depth_2d + sampler_comparisonWebGPU 将深度纹理与比较采样器解耦
texture(sampler2DShadow, vec3)textureSampleCompare(t, s, coord, ref)深度比较采样
gl.createTexture() + gl.texImage2D()device.createTexture()创建纹理
gl.RENDER_ATTACHMENTGPUTextureUsage.RENDER_ATTACHMENT渲染附件用途
gl.TEXTURE_2Dtexture_2d<f32> / texture_depth_2d纹理类型,深度纹理需显式声明
gl_FragDepth@builtin(frag_depth)片元深度输出
gl.clearDepth(1.0)depthClearValue: 1.0深度清除值

注意:WebGPU 的着色语言为 WGSL,而非 GLSL。虽然概念上接近,但 API 和语法均有差异。在移植 WebGL 阴影实现到 WebGPU 时,务必使用 texture_depth_2d、sampler_comparison 和 textureSampleCompare,并显式设置深度附件的 depthLoadOp / depthStoreOp。