246 lines
10 KiB
HLSL
246 lines
10 KiB
HLSL
// Screen-space ambient occlusion (GTAO) over the resolved native scene.
|
|
// Ground-truth ambient occlusion after Jimenez et al.: per pixel, a few
|
|
// screen-space slices each march the depth buffer both ways to find the
|
|
// horizon angles, and the cosine-weighted arc between them integrates to a
|
|
// visibility term. Normals are reconstructed from depth (no G-buffer), so
|
|
// the pass needs only the scene depth + the projection constants.
|
|
//
|
|
// Pass chain (all fullscreen triangles, own binding layout):
|
|
// ps_linearize scene depth (sample 0 when MSAA; AO_MSAA=1 variant)
|
|
// -> view-space Z, R32_FLOAT, full res
|
|
// ps_luma scene color -> luminance, R8_UNORM, AO raster (the
|
|
// sun-lit protection mask input)
|
|
// ps_gtao linear depth + luma -> the final gamma-encoded AO
|
|
// multiplier, R8_UNORM (fully-protected pixels skip the
|
|
// march, most of a daytime frame)
|
|
// ps_blur depth-aware separable gaussian (2 passes), R8_UNORM
|
|
// ps_composite AO onto the guest output via dst.rgb * src.a blending
|
|
// (RGB write mask; the output has no usable alpha anyway)
|
|
// ps_debug AO greyscale replace, for tuning
|
|
//
|
|
// Color space note: the scene target is gamma-encoded (the material shaders
|
|
// linearize by squaring and return via sqrt), so the composite applies
|
|
// sqrt(ao_linear): a linear-space multiply expressed in gamma space.
|
|
cbuffer C : register(b0) {
|
|
float4 size; // xy = output size in px, zw = 1 / size
|
|
float4 proj; // x = |m00|, y = |m11|, z = m22, w = m32 (row-vector proj)
|
|
float4 p0; // x = radius (view units), y = intensity,
|
|
// z = fade start (view Z), w = 1 / fade range
|
|
float4 p1; // xy = blur axis in px, z = sun-lit luma protection scale
|
|
};
|
|
// t0 is the per-pass primary input: the scene depth for ps_linearize, the
|
|
// linear-depth / AO intermediates for the march/blur, the scene color for
|
|
// copy/composite. Each entry references exactly one of the overlapping t0
|
|
// declarations; the others drop out at compile.
|
|
#ifdef AO_MSAA
|
|
Texture2DMS<float> depth_ms : register(t0);
|
|
#endif
|
|
Texture2D<float> tex0 : register(t0);
|
|
Texture2D<float4> tex0c : register(t0); // ps_luma scene color
|
|
Texture2D<float> tex1 : register(t1); // gtao: luma; blur: linear depth
|
|
SamplerState smp_point : register(s0);
|
|
SamplerState smp_linear : register(s1);
|
|
|
|
static const float kPi = 3.14159265f;
|
|
// 3 slices x 3 steps/side = 18 taps: the reference GTAO "high" budget. The
|
|
// per-pixel rotation + depth-aware blur integrate the dither; the nonlinear
|
|
// step distribution below spends the taps near the contact zone where the
|
|
// occlusion signal lives. (6 steps was ~2x the cost for a difference the
|
|
// blur erased; at night, where no pixel early-outs, that doubled the whole
|
|
// pass.)
|
|
#define AO_SLICES 3
|
|
#define AO_STEPS 3
|
|
|
|
struct VSOut {
|
|
float4 pos : SV_Position;
|
|
float2 uv : TEXCOORD0;
|
|
};
|
|
VSOut vs_main(uint id : SV_VertexID) {
|
|
VSOut o;
|
|
float2 uv = float2((id << 1) & 2, id & 2);
|
|
o.pos = float4(uv * float2(2, -2) + float2(-1, 1), 0, 1);
|
|
o.uv = uv;
|
|
return o;
|
|
}
|
|
|
|
// Device depth -> view-space Z. Row-vector projection with m23 = 1:
|
|
// ndc_z = m22 + m32 / z_view.
|
|
float LinearZ(float d) {
|
|
return clamp(proj.w / min(d - proj.z, -1e-6), 0.05f, 4e5f);
|
|
}
|
|
|
|
float ps_linearize(VSOut i) : SV_Target {
|
|
#ifdef AO_MSAA
|
|
float d = depth_ms.Load(int2(i.pos.xy), 0);
|
|
#else
|
|
float d = tex0.Load(int3(int2(i.pos.xy), 0));
|
|
#endif
|
|
return LinearZ(d);
|
|
}
|
|
|
|
// View-space position from UV + linear Z. |m00|/|m11| make the space a
|
|
// (possibly mirrored) view space; every AO quantity is invariant under a
|
|
// mirrored axis, so the projection's negative x scale needs no special
|
|
// handling.
|
|
float3 ViewPos(float2 uv, float z) {
|
|
float2 ndc = float2(uv.x * 2.0f - 1.0f, 1.0f - uv.y * 2.0f);
|
|
return float3(ndc.x * z / proj.x, ndc.y * z / proj.y, z);
|
|
}
|
|
|
|
float SampleZ(float2 uv) {
|
|
return tex0.SampleLevel(smp_point, uv, 0);
|
|
}
|
|
|
|
// Normal from depth: for each axis take the neighbor with the smaller depth
|
|
// delta (avoids smeared normals across silhouette edges), cross the two
|
|
// tangents, and face the camera.
|
|
float3 ReconstructNormal(float2 uv, float3 P) {
|
|
float2 dx = float2(size.z, 0.0f);
|
|
float2 dy = float2(0.0f, size.w);
|
|
float zl = SampleZ(uv - dx), zr = SampleZ(uv + dx);
|
|
float zu = SampleZ(uv - dy), zd = SampleZ(uv + dy);
|
|
float3 th = (abs(zr - P.z) < abs(zl - P.z))
|
|
? (ViewPos(uv + dx, zr) - P)
|
|
: (P - ViewPos(uv - dx, zl));
|
|
float3 tv = (abs(zd - P.z) < abs(zu - P.z))
|
|
? (ViewPos(uv + dy, zd) - P)
|
|
: (P - ViewPos(uv - dy, zu));
|
|
float3 n = normalize(cross(tv, th));
|
|
return (dot(n, -P) < 0.0f) ? -n : n;
|
|
}
|
|
|
|
// Spatial rotation/offset noise (interleaved gradient noise); the blur
|
|
// integrates the resulting high-frequency dither.
|
|
float Ign(float2 p) {
|
|
return frac(52.9829189f * frac(dot(p, float2(0.06711056f, 0.00583715f))));
|
|
}
|
|
|
|
// Half-res scene luminance for the sun-lit protection mask (low-frequency
|
|
// by nature, so the AO raster resolution is plenty; bilinear at the 2x
|
|
// reduction is an exact 2x2 box).
|
|
float ps_luma(VSOut i) : SV_Target {
|
|
float3 scene = tex0c.SampleLevel(smp_linear, i.uv, 0).rgb;
|
|
return dot(scene, float3(0.299f, 0.587f, 0.114f));
|
|
}
|
|
|
|
float ps_gtao(VSOut i) : SV_Target {
|
|
float zc = SampleZ(i.uv);
|
|
float fade = saturate((zc - p0.z) * p0.w);
|
|
// Sun-lit protection: real AO attenuates only ambient light; direct sun
|
|
// is unaffected, but this chain runs post-tonemap where the two are
|
|
// already summed. Approximate the split by luminance: bright pixels are
|
|
// sun-lit and resist the darkening (keeping characters from dragging a
|
|
// gray pool across lit pavement), dim pixels are ambient/shade-lit and
|
|
// keep it. Fully-protected or fully-faded pixels skip the march.
|
|
float luma = tex1.SampleLevel(smp_point, i.uv, 0);
|
|
float protect = saturate(luma * p1.z);
|
|
protect *= protect;
|
|
// Skip fully-protected (sun-lit), fully-faded, and near-black pixels; a
|
|
// multiplier on near-zero color is invisible, so night skies and deep
|
|
// shadow don't pay for the march either.
|
|
if (fade >= 1.0f || protect >= 0.999f || luma < 0.015f) {
|
|
return 1.0f;
|
|
}
|
|
float3 P = ViewPos(i.uv, zc);
|
|
float3 N = ReconstructNormal(i.uv, P);
|
|
float3 V = -normalize(P);
|
|
|
|
// World-space radius projected to pixels at this depth (vertical FOV
|
|
// scale), bounded so near surfaces don't walk the whole screen (large
|
|
// scattered footprints also thrash the texture cache; the dominant cost
|
|
// of this pass is tap locality, not ALU).
|
|
float radius_px = clamp(p0.x * proj.y / zc * size.y * 0.5f, 2.0f, 64.0f);
|
|
float noise_dir = Ign(i.pos.xy);
|
|
float noise_off = Ign(i.pos.xy + 41.13f);
|
|
float inv_r = 1.0f / max(p0.x, 1e-4f);
|
|
|
|
float vis = 0.0f;
|
|
[unroll] for (int s = 0; s < AO_SLICES; ++s) {
|
|
float phi = (float(s) + noise_dir) * (kPi / float(AO_SLICES));
|
|
float2 sdir = float2(cos(phi), sin(phi));
|
|
// The view-space direction a +sdir pixel step moves the surface point
|
|
// (screen y runs down, view y up; aspect via the per-axis pixel sizes).
|
|
float3 D = normalize(float3(sdir.x * size.z / proj.x,
|
|
-sdir.y * size.w / proj.y, 0.0f));
|
|
float3 axis = normalize(cross(D, V));
|
|
float3 ortho = normalize(D - dot(D, V) * V);
|
|
// Normal projected into the slice plane; its signed angle from V decides
|
|
// the hemisphere the horizon arc is clamped to.
|
|
float3 Np = N - axis * dot(N, axis);
|
|
float npl = length(Np);
|
|
if (npl < 1e-4f) {
|
|
vis += 1.0f;
|
|
continue;
|
|
}
|
|
float cos_n = clamp(dot(Np, V) / npl, -1.0f, 1.0f);
|
|
float n = sign(dot(Np, ortho)) * acos(cos_n);
|
|
|
|
// March both sides, tracking the max cos(angle to V) with a smooth
|
|
// range falloff so geometry near the radius edge fades instead of pops.
|
|
float2 hcos = float2(-1.0f, -1.0f);
|
|
[unroll] for (int side = 0; side < 2; ++side) {
|
|
float sgn = side == 0 ? -1.0f : 1.0f;
|
|
[loop] for (int j = 0; j < AO_STEPS; ++j) {
|
|
// Nonlinear step distribution: taps cluster toward the contact
|
|
// zone; the falloff discounts the far ones anyway.
|
|
float fj = (float(j) + noise_off) / float(AO_STEPS);
|
|
float t = radius_px * fj * sqrt(fj) + 1.0f;
|
|
float2 suv = i.uv + sgn * sdir * t * size.zw;
|
|
float3 Ps = ViewPos(suv, SampleZ(suv));
|
|
float3 d3 = Ps - P;
|
|
float dist2 = max(dot(d3, d3), 1e-8f);
|
|
float rinv = rsqrt(dist2);
|
|
float c = dot(d3, V) * rinv;
|
|
float dist = dist2 * rinv;
|
|
c = lerp(c, -1.0f, saturate(dist * inv_r * 3.333f - 2.333f));
|
|
hcos[side] = max(hcos[side], c);
|
|
}
|
|
}
|
|
float h1 = n + max(-acos(clamp(hcos.x, -1.0f, 1.0f)) - n, -0.5f * kPi);
|
|
float h2 = n + min(acos(clamp(hcos.y, -1.0f, 1.0f)) - n, 0.5f * kPi);
|
|
// Cosine-weighted arc integral between the two horizons (GTAO).
|
|
float a =
|
|
0.25f * ((-cos(2.0f * h1 - n) + cos_n + 2.0f * h1 * sin(n)) +
|
|
(-cos(2.0f * h2 - n) + cos_n + 2.0f * h2 * sin(n)));
|
|
vis += npl * a;
|
|
}
|
|
vis = saturate(vis / float(AO_SLICES));
|
|
// Bake the full multiplier here so the blur smooths the finished term and
|
|
// the composite is a plain blend: intensity exponent on the linear
|
|
// visibility, the extra 0.5 converting the multiply into the target's
|
|
// gamma-2 encoding, then the protection and distance fades toward 1.
|
|
float a = pow(vis, p0.y * 0.5f);
|
|
a = lerp(a, 1.0f, protect);
|
|
return lerp(a, 1.0f, fade);
|
|
}
|
|
|
|
// Depth-aware separable gaussian (sigma 2, radius 4): integrates the slice
|
|
// dither without bleeding AO across depth edges.
|
|
float ps_blur(VSOut i) : SV_Target {
|
|
const float w[5] = {1.0f, 0.8825f, 0.6065f, 0.3247f, 0.1353f};
|
|
float zc = tex1.SampleLevel(smp_point, i.uv, 0);
|
|
float acc = tex0.SampleLevel(smp_point, i.uv, 0);
|
|
float wsum = 1.0f;
|
|
[unroll] for (int k = 1; k <= 4; ++k) {
|
|
[unroll] for (int s = 0; s < 2; ++s) {
|
|
float2 suv = i.uv + (s == 0 ? -1.0f : 1.0f) * p1.xy * float(k) * size.zw;
|
|
float zs = tex1.SampleLevel(smp_point, suv, 0);
|
|
float wz = saturate(1.0f - abs(zs - zc) / (zc * 0.05f + 0.05f));
|
|
float wk = w[k] * wz;
|
|
acc += tex0.SampleLevel(smp_point, suv, 0) * wk;
|
|
wsum += wk;
|
|
}
|
|
}
|
|
return acc / wsum;
|
|
}
|
|
|
|
// dst.rgb * src.a blend (RGB write mask): the AO plane already carries the
|
|
// finished gamma-encoded, luma-protected, distance-faded multiplier.
|
|
float4 ps_composite(VSOut i) : SV_Target {
|
|
return float4(0.0f, 0.0f, 0.0f, tex0.SampleLevel(smp_linear, i.uv, 0));
|
|
}
|
|
|
|
float4 ps_debug(VSOut i) : SV_Target {
|
|
float ao = tex0.SampleLevel(smp_linear, i.uv, 0);
|
|
return float4(ao, ao, ao, 1.0f);
|
|
}
|