跳到主要内容
shadow-mapping · 代码
查看代码目录
Graphics Learning Lab Shadow Mapping / Shader source

Observe · Change · Recompute · Explain

画面使用的深度比较代码,就是这里的代码

Read-only · exact runtime source

下面的 GLSL 直接由交互视口导入,并按 light camera、depth pass、projection、compare、bias、pcf、csm 与 debug 分段。

这一课怎么学

  1. 观察
  2. 预测
  3. 改一个量
  4. 看证据
  5. 再读代码
01 · vertex

光源视角与矩阵链

camera pass 与 depth pass 共享 world → light view/projection。

// SECTION:light-camera - the camera and every caster share world space
uniform mat4 uLightMatrices[4];

varying vec3 vWorldPosition;
varying vec3 vWorldNormal;
varying float vViewDistance;
varying vec4 vLightClip0;
varying vec4 vLightClip1;
varying vec4 vLightClip2;
varying vec4 vLightClip3;

void main() {
  vec4 worldPosition = modelMatrix * vec4(position, 1.0);
  vWorldPosition = worldPosition.xyz;
  vWorldNormal = normalize(mat3(transpose(inverse(modelMatrix))) * normal);
  vViewDistance = abs((viewMatrix * worldPosition).z);
02 · fragment

Depth pass

深度纹理保存光源看到的最近表面。

precision highp float;

uniform sampler2D uShadowMaps[4];
uniform float uCascadeSplits[4];
uniform vec2 uShadowTexelSize;
uniform vec3 uLightDirection;
uniform vec3 uMaterialColor;
uniform float uConstantBias;
uniform float uSlopeBias;
uniform float uNormalBias;
uniform float uPcfRadius;
uniform int uPcfKernel;
uniform int uCascadeCount;
uniform int uDebugMode;
uniform bool uReceiveShadow;

varying vec3 vWorldPosition;
varying vec3 vWorldNormal;
varying float vViewDistance;
varying vec4 vLightClip0;
varying vec4 vLightClip1;
varying vec4 vLightClip2;
varying vec4 vLightClip3;

// SECTION:depth-pass - the depth texture stores the closest light-space surface
float sampleDepth(int cascade, vec2 uv) {
  if (cascade == 0) return texture2D(uShadowMaps[0], uv).r;
  if (cascade == 1) return texture2D(uShadowMaps[1], uv).r;
  if (cascade == 2) return texture2D(uShadowMaps[2], uv).r;
  return texture2D(uShadowMaps[3], uv).r;
}

vec4 selectedLightClip(int cascade) {
  if (cascade == 0) return vLightClip0;
  if (cascade == 1) return vLightClip1;
  if (cascade == 2) return vLightClip2;
  return vLightClip3;
}
03 · vertex

齐次除法与投影

保留每级 light clip 坐标,供 receiver 重建 UV。

// SECTION:projection - keep the light-space clip coordinates for the receiver pass
  vLightClip0 = uLightMatrices[0] * worldPosition;
  vLightClip1 = uLightMatrices[1] * worldPosition;
  vLightClip2 = uLightMatrices[2] * worldPosition;
  vLightClip3 = uLightMatrices[3] * worldPosition;
  gl_Position = projectionMatrix * viewMatrix * worldPosition;
}
04 · fragment

深度比较

比较 receiver depth 与 map depth,并处理越界。

// SECTION:compare - project light clip space into a shadow-map lookup
vec3 lightUvDepth(int cascade) {
  vec4 clip = selectedLightClip(cascade);
  float safeW = max(abs(clip.w), 0.00001) * (clip.w < 0.0 ? -1.0 : 1.0);
  return clip.xyz / safeW * 0.5 + 0.5;
}

int selectedCascade() {
  int cascade = 0;
  if (uCascadeCount > 1 && vViewDistance > uCascadeSplits[0]) cascade = 1;
  if (uCascadeCount > 2 && vViewDistance > uCascadeSplits[1]) cascade = 2;
  if (uCascadeCount > 3 && vViewDistance > uCascadeSplits[2]) cascade = 3;
  return min(cascade, uCascadeCount - 1);
}
05 · fragment

Bias 误差预算

constant、slope、normal 三项分别对应不同误差来源。

// SECTION:bias - slope and normal terms spend an explicit error budget
float shadowBias() {
  float ndotl = max(dot(normalize(vWorldNormal), normalize(uLightDirection)), 0.0);
  float grazing = 1.0 - ndotl;
  return max(uConstantBias, 0.0) + max(uSlopeBias, 0.0) * grazing + max(uNormalBias, 0.0) * grazing;
}
06 · fragment

PCF 过滤

固定核多次读取深度,样本数是可解释的性能成本。

// SECTION:pcf - a fixed kernel trades softness for depth reads
float filteredVisibility(int cascade, vec3 uvDepth, float bias) {
  if (uvDepth.x < 0.0 || uvDepth.x > 1.0 || uvDepth.y < 0.0 || uvDepth.y > 1.0 || uvDepth.z < 0.0 || uvDepth.z > 1.0) return 1.0;
  int radius = (uPcfKernel - 1) / 2;
  float visibility = 0.0;
  float samples = 0.0;
  for (int y = -2; y <= 2; y += 1) {
    for (int x = -2; x <= 2; x += 1) {
      if (abs(x) <= radius && abs(y) <= radius) {
        vec2 offset = vec2(float(x), float(y)) * uShadowTexelSize * uPcfRadius;
        float mapDepth = sampleDepth(cascade, uvDepth.xy + offset);
        visibility += uvDepth.z - bias <= mapDepth ? 1.0 : 0.0;
        samples += 1.0;
      }
    }
  }
  return visibility / max(samples, 1.0);
}

float shadowVisibility(out int cascade, out vec3 uvDepth, out float bias) {
  cascade = selectedCascade();
  uvDepth = lightUvDepth(cascade);
  bias = shadowBias();
  if (selectedLightClip(cascade).w <= 0.0) return 1.0;
  return filteredVisibility(cascade, uvDepth, bias);
}
07 · fragment

CSM 级联选择

按视距选择 practical split,近景获得更多 texel。

// SECTION:csm - practical splits spend more texels near the camera
vec3 cascadeColor(int cascade) {
  if (cascade == 0) return vec3(0.95, 0.35, 0.25);
  if (cascade == 1) return vec3(0.35, 0.85, 0.42);
  if (cascade == 2) return vec3(0.32, 0.55, 1.0);
  return vec3(0.9, 0.55, 0.2);
}
08 · fragment

调试输出

把 depth、UV、cascade、acne 与 bias 直接编码成画面。

// SECTION:debug - expose depth, UV, cascade and bias evidence
void main() {
  int cascade = 0;
  vec3 uvDepth = vec3(0.0);
  float bias = 0.0;
  float visibility = shadowVisibility(cascade, uvDepth, bias);
  vec3 normal = normalize(vWorldNormal);
  vec3 light = normalize(uLightDirection);
  float diffuse = max(dot(normal, light), 0.0);
  vec3 base = uMaterialColor * (0.12 + diffuse * 0.88);
  vec3 color = base * mix(0.38, 1.0, visibility);

  if (uDebugMode == 1) color = vec3(sampleDepth(cascade, uvDepth.xy));
  if (uDebugMode == 2) color = vec3(uvDepth.xy, 1.0 - step(0.0, uvDepth.z) * step(uvDepth.z, 1.0));
  if (uDebugMode == 3) color = cascadeColor(cascade);
  if (uDebugMode == 4) color = vec3(uvDepth.z);
  if (uDebugMode == 5) color = vec3(1.0 - visibility, visibility * 0.35, 0.05);
  if (uDebugMode == 6) color = vec3(min(bias * 24.0, 1.0));

  if (!uReceiveShadow) color = base;
  gl_FragColor = vec4(color, 1.0);
  #include <tonemapping_fragment>
  #include <colorspace_fragment>
}