Observe · Change · Recompute · Explain
画面使用的深度比较代码,就是这里的代码
Read-only · exact runtime source
下面的 GLSL 直接由交互视口导入,并按 light camera、depth pass、projection、compare、bias、pcf、csm 与 debug 分段。
这一课怎么学
- 观察
- 预测
- 改一个量
- 看证据
- 再读代码
光源视角与矩阵链
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);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;
}齐次除法与投影
保留每级 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;
}深度比较
比较 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);
}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;
}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);
}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);
}调试输出
把 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>
}