跳到主要内容
screen-space · 代码
Graphics Learning Lab Screen-Space / Shader source

看见反常 · 动一处 · 抓证据

从 G-buffer 读出来的每一个证据

Read-only · exact runtime source

这页解决什么

代码直接来自交互视口,按 projection、gbuffer、reconstruct、ssao、ssr、bounds、debug 的顺序阅读。

01 · vertex

屏幕坐标与投影

全屏三角形让每个 fragment 拿到稳定的 UV。

varying vec2 vUv;

void main() {
  // SECTION:projection - a full-screen triangle carries the screen coordinate
  vUv = uv;
  gl_Position = vec4(position.xy, 0.0, 1.0);
}
02 · fragment

G-buffer attachments

albedo、normal 与 depth 是不同的证据来源。

precision highp float;

uniform sampler2D uColorTexture;
uniform sampler2D uDepthTexture;
uniform sampler2D uNormalTexture;
uniform sampler2D uPositionTexture;
uniform vec2 uTexelSize;
uniform float uNearPlane;
uniform float uFarPlane;
uniform float uSsaoRadius;
uniform float uSsaoBias;
uniform float uSsaoStrength;
uniform float uSsaoFalloff;
uniform int uSsaoSamples;
uniform bool uSsaoEnabled;
uniform float uSsrStepSize;
uniform float uSsrThickness;
uniform float uSsrMaxDistance;
uniform float uSsrEdgeFade;
uniform int uSsrMaxSteps;
uniform bool uSsrEnabled;
uniform int uDebugMode;
uniform int uProjection;
uniform int uPositionSource;
uniform float uTime;
uniform mat4 uProjectionMatrix;
uniform mat4 uInverseProjection;
uniform mat4 uInverseView;

varying vec2 vUv;

// SECTION:gbuffer - the color, normal and depth attachments are sampled independently
vec4 readAlbedo(vec2 uv) { return texture2D(uColorTexture, uv); }
float readDeviceDepth(vec2 uv) { return texture2D(uDepthTexture, uv).r; }
vec3 readNormal(vec2 uv) { return normalize(texture2D(uNormalTexture, uv).xyz * 2.0 - 1.0); }

float linearDepth(float depth) {
  if (uProjection == 1) return mix(uNearPlane, uFarPlane, depth);
  return (uNearPlane * uFarPlane) / max(uFarPlane - depth * (uFarPlane - uNearPlane), 0.0001);
}

vec3 reconstructViewPosition(vec2 uv, float depth) {
  vec4 clip = vec4(uv * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
  vec4 view = uInverseProjection * clip;
  return view.xyz / max(abs(view.w), 0.0001);
}

vec3 reconstructWorldPosition(vec2 uv, float depth) {
  vec4 world = uInverseView * vec4(reconstructViewPosition(uv, depth), 1.0);
  return world.xyz / max(abs(world.w), 0.0001);
}

vec3 readPosition(vec2 uv, float depth) {
  return uPositionSource == 1 ? texture2D(uPositionTexture, uv).xyz : reconstructViewPosition(uv, depth);
}
03 · fragment

深度重建位置

device depth 通过 inverse projection 回到 view/world space。

// SECTION:reconstruct - depth is nonlinear in perspective projection, then inverse projection restores position
float depthDelta(vec2 uv, float center) {
  float sampleDepth = readDeviceDepth(uv);
  return linearDepth(center) - linearDepth(sampleDepth);
}

float screenEdgeFade(vec2 uv) {
  float edge = min(min(uv.x, 1.0 - uv.x), min(uv.y, 1.0 - uv.y));
  return uSsrEdgeFade <= 0.0 ? 1.0 : clamp(edge / uSsrEdgeFade, 0.0, 1.0);
}

// Only the cyan teaching mirror receives SSR. World position prevents the
// original object from receiving its own sampled color; world normal keeps
// vertical surfaces out of the receiver even when they overlap the tile in UV.
float reflectionReceiverMask(vec3 worldPosition, vec3 viewNormal) {
  vec3 worldNormal = normalize(mat3(uInverseView) * viewNormal);
  float facesUp = smoothstep(0.82, 0.96, worldNormal.y);
  float mirrorHeight = 1.0 - smoothstep(0.045, 0.10, abs(worldPosition.y + 1.365));
  float insideX = 1.0 - smoothstep(2.92, 3.04, abs(worldPosition.x));
  float insideZ = 1.0 - smoothstep(1.49, 1.62, abs(worldPosition.z - 0.35));
  return facesUp * mirrorHeight * insideX * insideZ;
}

float reflectionSourceMask(vec3 worldPosition) {
  float insideX = 1.0 - smoothstep(1.18, 1.32, abs(worldPosition.x));
  float insideZ = 1.0 - smoothstep(1.16, 1.32, abs(worldPosition.z + 1.65));
  float aboveFloor = smoothstep(-1.44, -1.34, worldPosition.y);
  float belowTop = 1.0 - smoothstep(1.12, 1.28, worldPosition.y);
  return insideX * insideZ * aboveFloor * belowTop;
}
04 · fragment

SSAO 采样

有限邻域与有效样本决定 AO 的可信范围。

// SECTION:ssao - bounded samples turn nearby depth differences into an occlusion estimate
float computeSsao(vec2 uv, float centerDepth) {
  if (!uSsaoEnabled) return 0.0;
  float occluded = 0.0;
  float valid = 0.0;
  vec2 golden = vec2(0.754877666, 0.569840296);
  for (int index = 0; index < 32; index += 1) {
    if (index >= uSsaoSamples) break;
    float angle = float(index) * 6.2831853 * golden.x + uTime * 0.08;
    float radius = uSsaoRadius * (0.35 + 0.65 * (float(index) + 1.0) / float(max(uSsaoSamples, 1)));
    // Radius is converted to a local pixel footprint. Multiplying by the full
    // viewport cancelled uTexelSize and sampled most of the screen, producing
    // giant duplicate silhouettes that looked like reflection ghosting.
    vec2 sampleUv = uv + vec2(cos(angle), sin(angle)) * uTexelSize * radius * 24.0;
    if (sampleUv.x < 0.0 || sampleUv.x > 1.0 || sampleUv.y < 0.0 || sampleUv.y > 1.0) continue;
    valid += 1.0;
    float delta = depthDelta(sampleUv, centerDepth);
    occluded += step(uSsaoBias, delta);
  }
  float ratio = occluded / max(valid, 1.0);
  return clamp(pow(ratio, max(uSsaoFalloff, 0.01)) * uSsaoStrength, 0.0, 1.0);
}
05 · fragment

SSR 步进

沿反射方向读取屏幕深度,命中后混合反射颜色。

// SECTION:ssr - march a reflected screen ray until the depth buffer supplies a hit
vec4 traceSsr(vec2 uv, float centerDepth, vec3 normal) {
  if (!uSsrEnabled) return vec4(0.0);
  vec3 viewPosition = reconstructViewPosition(uv, centerDepth);
  vec3 viewDirection = normalize(-viewPosition);
  vec3 reflected = normalize(reflect(-viewDirection, normal));
  for (int index = 1; index <= 64; index += 1) {
    if (index > uSsrMaxSteps) break;
    float distance = float(index) * uSsrStepSize;
    if (distance > uSsrMaxDistance) break;
    vec3 rayPosition = viewPosition + reflected * distance;
    vec4 rayClip = uProjectionMatrix * vec4(rayPosition, 1.0);
    if (rayClip.w <= 0.0) return vec4(0.0);
    vec3 rayNdc = rayClip.xyz / rayClip.w;
    vec2 sampleUv = rayNdc.xy * 0.5 + 0.5;
    if (sampleUv.x < 0.0 || sampleUv.x > 1.0 || sampleUv.y < 0.0 || sampleUv.y > 1.0) return vec4(0.0);
    float sampleDepth = readDeviceDepth(sampleUv);
    if (sampleDepth >= 0.99999) continue;
    vec3 sampleViewPosition = reconstructViewPosition(sampleUv, sampleDepth);
    vec3 sampleWorldPosition = reconstructWorldPosition(sampleUv, sampleDepth);
    // Reject the mirror/ground itself so the first sample cannot become a
    // blurred copy of the current pixel (the old "ghost image" failure).
    if (sampleWorldPosition.y < -1.22) continue;
    float rayLinearDepth = -rayPosition.z;
    float sampleLinearDepth = -sampleViewPosition.z;
    float crossedSurface = rayLinearDepth - sampleLinearDepth;
    if (index > 1 && crossedSurface >= 0.0 && crossedSurface <= uSsrThickness) {
      float confidence = screenEdgeFade(sampleUv);
      return vec4(readAlbedo(sampleUv).rgb, confidence);
    }
  }
  return vec4(0.0);
}
06 · fragment

屏幕边界与失败

屏幕外没有几何证据,edge fade 让 miss 可见。

// SECTION:bounds - screen-space effects expose their own validity and edge fade
vec3 boundsDebug(vec2 uv, vec4 ssr) {
  float fade = screenEdgeFade(uv);
  vec3 miss = vec3(0.88, 0.18, 0.16);
  vec3 hit = vec3(0.18, 0.88, 0.52);
  vec3 status = ssr.a > 0.001 ? hit : miss;
  return mix(vec3(0.96, 0.68, 0.18), status, smoothstep(0.2, 0.72, fade));
}
07 · fragment

Debug views

将位置、深度、AO、SSR 结果直接输出为可观察证据。

// SECTION:debug - switch from the final composite to evidence views for teaching
void main() {
  vec4 albedo = readAlbedo(vUv);
  float deviceDepth = readDeviceDepth(vUv);
  float linear = linearDepth(deviceDepth);
  vec3 normal = readNormal(vUv);
  vec3 viewPosition = readPosition(vUv, deviceDepth);
  vec3 worldPosition = reconstructWorldPosition(vUv, deviceDepth);
  float receiverMask = reflectionReceiverMask(worldPosition, normal);
  float sourceMask = reflectionSourceMask(worldPosition);
  float ao = computeSsao(vUv, deviceDepth);
  vec4 ssr = receiverMask > 0.001 ? traceSsr(vUv, deviceDepth, normal) : vec4(0.0);

  vec3 color = albedo.rgb;
  color *= 1.0 - ao * 0.72;
  color = mix(color, ssr.rgb, receiverMask * ssr.a * 0.72);

  if (uDebugMode == 1) color = albedo.rgb;
  if (uDebugMode == 2) color = normal * 0.5 + 0.5;
  if (uDebugMode == 3) color = normalize(worldPosition) * 0.5 + 0.5;
  if (uDebugMode == 4) color = vec3(deviceDepth);
  if (uDebugMode == 5) color = vec3(clamp(linear / uFarPlane, 0.0, 1.0));
  if (uDebugMode == 6) color = vec3(1.0 - ao, 0.24 + ao * 0.65, 0.08);
  if (uDebugMode == 7) {
    float gray = dot(albedo.rgb, vec3(0.2126, 0.7152, 0.0722));
    vec3 quietScene = mix(vec3(0.025, 0.038, 0.052), vec3(gray), 0.28);
    vec3 sourceReference = mix(quietScene, albedo.rgb, sourceMask);
    vec3 reflectionOnly = mix(vec3(0.018, 0.035, 0.045), ssr.rgb, ssr.a);
    color = mix(sourceReference, reflectionOnly, receiverMask);
  }
  if (uDebugMode == 8) {
    vec3 sceneReference = mix(vec3(0.025, 0.035, 0.05), albedo.rgb, 0.24);
    color = mix(sceneReference, boundsDebug(vUv, ssr), receiverMask);
  }

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