478 lines
22 KiB
HLSL
478 lines
22 KiB
HLSL
// HDR post chain: bloom pyramid + the scene tonemap, applied once over the
|
|
// float scene intermediate (see the HDR path in skate3_native_scene.cpp).
|
|
//
|
|
// The material shaders' shared tone chain, fog -> exposure -> tonemap ->
|
|
// sqrt -> the postfx uber's 1.41 scene multiplier, is IDENTICAL across every
|
|
// branch after the per-branch exposure multiply, so under HDR the branches
|
|
// write that chain's INPUT (pre-tonemap linear, "xe") into an RGBA16F target
|
|
// and this pass applies the chain exactly once. Bloom energy is injected
|
|
// pre-tonemap (x + bloom * intensity), so intensity 0 reproduces the classic
|
|
// in-material output bit-for-bit modulo fp16 storage rounding.
|
|
//
|
|
// Pass chain (fullscreen triangles, own binding layout = the SSAO shape):
|
|
// ps_bloom_down 13-tap Jimenez downsample; the BLOOM_FIRST=1 variant adds
|
|
// the soft-knee threshold + per-group Karis luma weighting
|
|
// (anti-firefly) on the mip-0 extraction
|
|
// ps_bloom_up 3x3 tent upsample, additive-blended onto the next-larger
|
|
// level (ONE/ONE at the PSO)
|
|
// ps_tonemap scene xe + bloom + volumetric terms -> the exact classic
|
|
// tone chain -> the gamma guest output
|
|
//
|
|
// Volumetric lighting (shadow-marched sun shafts + directional haze) rides
|
|
// the same layout plus the frame's shadow constant slice at b1:
|
|
// ps_vol_linearize scene depth -> linear view Z, full res (only when the
|
|
// SSAO pass did not already produce the plane this frame;
|
|
// VOL_MSAA=1 variant for the MSAA depth buffer)
|
|
// ps_vol_shafts half res: reconstruct the world position from depth,
|
|
// march the camera->pixel ray with per-pixel jitter and
|
|
// test each step's sun visibility against the CSM atlas
|
|
// and the native static sun-shadow map (falling back to
|
|
// the baked world-shadow map outside its coverage),
|
|
// real shadowed air,
|
|
// so occlusion never depends on on-screen silhouettes
|
|
// (no ghosting around near characters) and beams appear
|
|
// wherever shadow volumes cross the view ray.
|
|
// The shaft plane and a full-res analytic haze term (Schlick phase on the
|
|
// view/sun angle x depth scattering, tinted by the frame's captured fog
|
|
// color) both join in ps_tonemap AFTER the AO multiply; in-air light is
|
|
// not subject to surface occlusion, and before the tone chain, so they
|
|
// bloom and tonemap like scene light.
|
|
cbuffer C : register(b0) {
|
|
float4 size; // xy = destination size in px, zw = 1 / destination size
|
|
float4 src; // xy = source size in px, zw = 1 / source size
|
|
float4 p0; // x = bloom threshold (xe space), y = soft knee,
|
|
// z = bloom intensity, w = debug (1 = bloom term, 2 = raw xe,
|
|
// 3 = AO plane, 4 = shaft plane, 5 = haze term)
|
|
float4 p1; // x = downsample tap spread in source texels (2 on the 4x
|
|
// extraction so the 13-tap footprint covers the whole
|
|
// source block, 1 on the 2x chain), yzw spare
|
|
// Volumetric rows (zero when the effect is off; every term drops out).
|
|
// ps_vol_shafts overloads the tail with the world unprojection instead:
|
|
// src = camera world position (xyz) + march reach, p0 = march steps /
|
|
// strength / phase g, p1.xy = proj m22 / m32, vp/vs0/vs1/vs2 = the
|
|
// row-vector inverse view-projection (world = clip * M).
|
|
float4 vp; // tonemap/linearize: x = |m00|, y = |m11|, z = m22, w = m32
|
|
float4 vs0; // tonemap: x = shaft plane resolution divisor (>= 2),
|
|
// w = shaft intensity (yz unused)
|
|
float4 vs1; // tonemap: xyz = haze tint (xe space), w = haze intensity
|
|
float4 vs2; // tonemap: xyz = sun direction in AO view space (toward the
|
|
// sun), w = haze density (1 / view unit)
|
|
};
|
|
// Per-frame shadow / lighting constant slice (the scene pass's b1) -
|
|
// ps_vol_shafts samples sun visibility with the exact receiver rows
|
|
// the materials use.
|
|
#include "scene_frame_cb.hlsli"
|
|
Texture2D<float4> tex0 : register(t0); // per-pass primary input
|
|
#ifdef VOL_MSAA
|
|
Texture2DMS<float> depth_ms : register(t0); // ps_vol_linearize MSAA depth
|
|
#endif
|
|
Texture2D<float> tex0d : register(t0); // ps_vol_linearize 1x depth
|
|
Texture2D<float4> tex1 : register(t1); // ps_tonemap: the bloom plane
|
|
Texture2D<float> tex1d : register(t1); // ps_vol_shafts: linear depth
|
|
// ps_tonemap: the SSAO multiplier plane (fused pre-tonemap apply, replaces
|
|
// the classic path's separate full-res composite draw; white when AO is
|
|
// off). Bilinear upsample from the AO raster, like the classic composite.
|
|
Texture2D<float> ao_plane : register(t2);
|
|
// ps_vol_shafts: the CSM atlas (x = light-space depth, y = coverage).
|
|
Texture2D<float2> shadow_tex : register(t2);
|
|
// ps_tonemap: the half-res shaft plane (white with vs0.w = 0 when shafts
|
|
// did not run) and the full-res linear view-Z plane (white with vs1.w = 0
|
|
// when no depth was produced this frame).
|
|
Texture2D<float4> vol_plane : register(t3);
|
|
// ps_vol_shafts: the 512x512 static world-shadow map (x = light-space
|
|
// depth; sge(stored >= ray) = lit, uncovered texels hold the far clear).
|
|
Texture2D<float4> ws_tex : register(t3);
|
|
Texture2D<float> lin_depth : register(t4);
|
|
// ps_vol_shafts: the native static sun-shadow atlas (the scene pass's t10,
|
|
// three tiles inner/mid/far left to right, x = depth along the material
|
|
// sun). White fallback = the far clear = lit everywhere when the map was
|
|
// not rendered this frame.
|
|
Texture2D<float2> nsm_tex : register(t4);
|
|
SamplerState smp_point : register(s0);
|
|
SamplerState smp_linear : register(s1);
|
|
|
|
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;
|
|
}
|
|
|
|
// The exact scene tone chain (see scene.hlsl ToneOut): input = the
|
|
// materials' post-fog, post-exposure linear value.
|
|
float3 ToneMapScene(float3 x) {
|
|
float3 t1 = saturate(1.0 - x);
|
|
float3 tm = max(x * 0.25 + 0.75, 1.0) - t1 * t1;
|
|
return saturate(sqrt(max(tm * 0.5, 0.0)) * 1.41);
|
|
}
|
|
|
|
// Device depth -> view-space Z. Row-vector projection with m23 = 1:
|
|
// ndc_z = m22 + m32 / z_view.
|
|
float LinearZ(float d) {
|
|
return clamp(vp.w / min(d - vp.z, -1e-6), 0.05f, 4e5f);
|
|
}
|
|
|
|
// View-space position from UV + linear Z (|m00|/|m11| make this the SSAO
|
|
// passes' possibly-mirrored view space; the sun direction row is folded
|
|
// into the same space CPU-side, so angles stay consistent).
|
|
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 / vp.x, ndc.y * z / vp.y, z);
|
|
}
|
|
|
|
// Interleaved gradient noise: per-pixel march phase jitter (the bilinear
|
|
// half-res upsample integrates the dither).
|
|
float Ign(float2 p) {
|
|
return frac(52.9829189f * frac(dot(p, float2(0.06711056f, 0.00583715f))));
|
|
}
|
|
|
|
float ps_vol_linearize(VSOut i) : SV_Target {
|
|
#ifdef VOL_MSAA
|
|
float d = depth_ms.Load(int2(i.pos.xy), 0);
|
|
#else
|
|
float d = tex0d.Load(int3(int2(i.pos.xy), 0));
|
|
#endif
|
|
return LinearZ(d);
|
|
}
|
|
|
|
// CSM atlas sun visibility at a world position: the material receiver
|
|
// math (see scene.hlsl SampleCsmShadow) with the dynamicobject per-cascade
|
|
// biases (air samples are not casters; the world bias is 0 in captures).
|
|
// Returns -1 outside every cascade: uncovered air is EXCLUDED from the
|
|
// march average rather than assumed lit, so out-of-range distance cannot
|
|
// wash the beams out.
|
|
float CsmVis(float3 wp) {
|
|
float2 lsv = float2(dot(sh_x.xyz, wp) + sh_x.w, dot(sh_y.xyz, wp) + sh_y.w);
|
|
float2 luv = 0.0;
|
|
float casc = 0.0;
|
|
float2 l2 = lsv * sh_c2.xy + sh_c2.zw;
|
|
if (max(abs(l2.x), abs(l2.y)) < 0.99) { luv = l2; casc = 3.0; }
|
|
float2 l1 = lsv * sh_c1.xy + sh_c1.zw;
|
|
if (max(abs(l1.x), abs(l1.y)) < 0.99) { luv = l1; casc = 2.0; }
|
|
if (max(abs(lsv.x), abs(lsv.y)) < 0.99) { luv = lsv; casc = 1.0; }
|
|
if (casc <= 0.0) {
|
|
return -1.0;
|
|
}
|
|
float2 suv = float2(luv.x / 6.0 + (casc * 2.0 - 1.0) / 6.0,
|
|
luv.y * -0.5 + 0.5);
|
|
float rd = dot(sh_z.xyz, wp) + sh_z.w - (casc > 1.5 ? 0.015 : 0.007);
|
|
float2 sm2 = shadow_tex.SampleLevel(smp_linear, suv, 0);
|
|
return saturate((sm2.x >= rd ? 1.0 : 0.0) + (1.0 - sm2.y));
|
|
}
|
|
|
|
// Static world-shadow (region-wide baked building/tree shade) visibility;
|
|
// -1 outside the map's projection.
|
|
float WorldVis(float3 wp) {
|
|
float4 wp4 = float4(wp, 1.0);
|
|
float2 wuv = float2(dot(wp4, dyn_wsx) * 0.5 + 0.5,
|
|
dot(wp4, dyn_wsy) * -0.5 + 0.5);
|
|
if (any(saturate(wuv) != wuv)) {
|
|
return -1.0;
|
|
}
|
|
return ws_tex.SampleLevel(smp_point, wuv, 0).x >= dot(wp4, dyn_wsz)
|
|
? 1.0
|
|
: 0.0;
|
|
}
|
|
|
|
// Native static sun-shadow visibility (nsm_tex, the scene pass's t10): the
|
|
// static world rendered along the TRUE material sun, so static-geometry
|
|
// shafts align with the sun glow; the baked ws map above projects along
|
|
// the game's near-vertical utility axis, ~40 degrees off the sun, which
|
|
// skews its shadow volumes accordingly. Cascade select mirrors
|
|
// SampleStaticSun (three tiles sharing the origin, ratios 6/2/1); a single
|
|
// soft-free compare per step is enough for the jittered march average.
|
|
// Returns -1 outside the map or with the feature off. Uncovered texels
|
|
// hold the far clear = lit.
|
|
float NsmVis(float3 wp) {
|
|
if (nsm_p.x <= 0.0) {
|
|
return -1.0;
|
|
}
|
|
float2 uc_far = float2(dot(nsm_x.xyz, wp) + nsm_x.w,
|
|
dot(nsm_y.xyz, wp) + nsm_y.w);
|
|
if (max(abs(uc_far.x), abs(uc_far.y)) >= 0.99) {
|
|
return -1.0;
|
|
}
|
|
float2 uc0 = uc_far * 6.0;
|
|
float2 uc1 = uc_far * 2.0;
|
|
float tile;
|
|
float2 uc;
|
|
if (max(abs(uc0.x), abs(uc0.y)) < 0.99) {
|
|
tile = 0.0;
|
|
uc = uc0;
|
|
} else if (max(abs(uc1.x), abs(uc1.y)) < 0.99) {
|
|
tile = 1.0;
|
|
uc = uc1;
|
|
} else {
|
|
tile = 2.0;
|
|
uc = uc_far;
|
|
}
|
|
float2 suv = float2(uc.x / 6.0 + (tile * 2.0 + 1.0) / 6.0,
|
|
uc.y * -0.5 + 0.5);
|
|
// Keep the bilinear tap off the tile seams (the atlas is 3 tiles wide).
|
|
float hx = 0.1666667 / nsm_p2.w;
|
|
suv.x = clamp(suv.x, tile / 3.0 + hx, tile / 3.0 + 0.3333333 - hx);
|
|
float refd = dot(nsm_z.xyz, wp) + nsm_z.w - nsm_p2.x;
|
|
float s = nsm_tex.SampleLevel(smp_linear, suv, 0).x >= refd ? 1.0 : 0.0;
|
|
// The material receive bottoms out at 1 - strength; shadowed air follows
|
|
// the same authored blend so the shafts fade with the surface shadows.
|
|
return 1.0 - nsm_p.x * (1.0 - s);
|
|
}
|
|
|
|
// Shadow-marched volumetric sun shafts (half res). The camera->pixel ray
|
|
// is marched with per-pixel jitter; each step's sun visibility comes from
|
|
// the CSM atlas plus the native static sun map (baked ws map outside its
|
|
// coverage), so the beams are cast by real shadow volumes, never by
|
|
// on-screen silhouettes. Output = the
|
|
// SHADOWED fraction x Henyey-Greenstein forward-scatter phase x marched
|
|
// path fraction: the game's sky and fog already carry the authored
|
|
// scattering of fully-lit air, so adding lit-air glow double-counts it
|
|
// into a structureless veil; only the shadow volumes' CUTS into that
|
|
// glow are new information, and ps_tonemap SUBTRACTS them (crepuscular
|
|
// shafts by contrast). Fully lit air contributes exactly zero.
|
|
float4 ps_vol_shafts(VSOut i) : SV_Target {
|
|
float d = tex1d.SampleLevel(smp_point, i.uv, 0);
|
|
// World position via the inverse view-projection (row-vector): the clip
|
|
// vector is (ndc * w, ndc_z * w, w) with w = view Z and ndc_z = m22 +
|
|
// m32 / z.
|
|
float2 ndc = float2(i.uv.x * 2.0 - 1.0, 1.0 - i.uv.y * 2.0);
|
|
float zn = p1.x + p1.y / max(d, 1e-4);
|
|
float4 clip = float4(ndc.x * d, ndc.y * d, zn * d, d);
|
|
float4 wp4 = clip.x * vp + clip.y * vs0 + clip.z * vs1 + clip.w * vs2;
|
|
float3 wp = wp4.xyz / max(wp4.w, 1e-6);
|
|
float3 cam = src.xyz;
|
|
float3 ray = wp - cam;
|
|
float dist = length(ray);
|
|
float3 rdir = ray / max(dist, 1e-4);
|
|
float reach = min(dist, src.w);
|
|
// Constant per-meter sample density: a short ray to near geometry needs
|
|
// proportionally fewer steps for the same marched quality (the fixed
|
|
// count oversampled a 5 m ray 8x versus the full-reach case), and the
|
|
// path-fraction scale keeps near-surface terms small enough that the
|
|
// extra variance stays under the tent blur.
|
|
float steps_max = clamp(p0.x, 8.0, 64.0);
|
|
float steps = clamp(ceil(steps_max * reach / max(src.w, 1.0)), 8.0,
|
|
steps_max);
|
|
float jitter = Ign(i.pos.xy);
|
|
float lit = 0.0;
|
|
float tot = 0.0;
|
|
[loop] for (int k = 0; k < 64; ++k) {
|
|
if (k >= (int)steps) {
|
|
break;
|
|
}
|
|
float3 sp = cam + rdir * ((float(k) + jitter) / steps * reach);
|
|
float cv = CsmVis(sp);
|
|
// Static shade: the native sun map where it covers (true material-sun
|
|
// axis), else the baked ws map. Never both: inside the sun map's range
|
|
// the two would draw the same caster's shadow volume on two different
|
|
// axes and read as a doubled shaft.
|
|
float nv = NsmVis(sp);
|
|
float sv = nv >= 0.0 ? nv : WorldVis(sp);
|
|
if (cv >= 0.0 || sv >= 0.0) {
|
|
lit += min(cv < 0.0 ? 1.0 : cv, sv < 0.0 ? 1.0 : sv);
|
|
tot += 1.0;
|
|
}
|
|
}
|
|
if (tot < 2.5) {
|
|
return float4(0.0, 0.0, 0.0, 1.0);
|
|
}
|
|
float ct = dot(rdir, sh_sun.xyz);
|
|
float g = p0.z;
|
|
// Henyey-Greenstein SHAPE without the 4-pi radiance normalization; the
|
|
// output is a dimming factor, not integrated radiance, so the phase only
|
|
// steers directionality: ~1.25 at 50 degrees off the sun, capped toward
|
|
// the sun axis (the 4-pi-normalized form peaked at 0.03-0.1 at typical
|
|
// viewing angles and made the effect invisible at any intensity).
|
|
float hg = min((1.0 - g * g) /
|
|
pow(max(1.0 + g * g - 2.0 * g * ct, 1e-3), 1.5),
|
|
2.0);
|
|
// Path fraction: a near surface has little air in front of it; the term
|
|
// that keeps the shafts off close characters.
|
|
float e = (1.0 - lit / tot) * hg * saturate(reach / max(src.w, 1.0));
|
|
return float4(e, e, e, 1.0);
|
|
}
|
|
|
|
// Soft-knee brightpass (kept in xe space, where 1.0 is the tonemapper's
|
|
// saturation point): quadratic ramp from (threshold - knee) to
|
|
// (threshold + knee), linear excess beyond.
|
|
float3 SoftThreshold(float3 c) {
|
|
float br = max(c.r, max(c.g, c.b));
|
|
float rq = clamp(br - p0.x + p0.y, 0.0, 2.0 * p0.y);
|
|
rq = rq * rq / (4.0 * p0.y + 1e-4);
|
|
return c * (max(rq, br - p0.x) / max(br, 1e-4));
|
|
}
|
|
|
|
// 13-tap downsample (Jimenez, SIGGRAPH 2014): five overlapping 2x2 groups,
|
|
// the half-texel-centered inner group at weight 0.5 and four outer groups at
|
|
// 0.125 each. On the first (extraction) pass each group is Karis-weighted by
|
|
// 1/(1 + luma) before mixing, which stops single HDR-bright samples
|
|
// (specular fireflies, the sun-core texels) from flickering as they cross
|
|
// pyramid texel grids.
|
|
float4 ps_bloom_down(VSOut i) : SV_Target {
|
|
float2 ts = src.zw * p1.x;
|
|
float3 tA = tex0.SampleLevel(smp_linear, i.uv + ts * float2(-2, -2), 0).rgb;
|
|
float3 tB = tex0.SampleLevel(smp_linear, i.uv + ts * float2(0, -2), 0).rgb;
|
|
float3 tC = tex0.SampleLevel(smp_linear, i.uv + ts * float2(2, -2), 0).rgb;
|
|
float3 tD = tex0.SampleLevel(smp_linear, i.uv + ts * float2(-1, -1), 0).rgb;
|
|
float3 tE = tex0.SampleLevel(smp_linear, i.uv + ts * float2(1, -1), 0).rgb;
|
|
float3 tF = tex0.SampleLevel(smp_linear, i.uv + ts * float2(-2, 0), 0).rgb;
|
|
float3 tG = tex0.SampleLevel(smp_linear, i.uv, 0).rgb;
|
|
float3 tH = tex0.SampleLevel(smp_linear, i.uv + ts * float2(2, 0), 0).rgb;
|
|
float3 tI = tex0.SampleLevel(smp_linear, i.uv + ts * float2(-1, 1), 0).rgb;
|
|
float3 tJ = tex0.SampleLevel(smp_linear, i.uv + ts * float2(1, 1), 0).rgb;
|
|
float3 tK = tex0.SampleLevel(smp_linear, i.uv + ts * float2(-2, 2), 0).rgb;
|
|
float3 tL = tex0.SampleLevel(smp_linear, i.uv + ts * float2(0, 2), 0).rgb;
|
|
float3 tM = tex0.SampleLevel(smp_linear, i.uv + ts * float2(2, 2), 0).rgb;
|
|
float3 g0 = (tD + tE + tI + tJ) * 0.25; // inner group
|
|
float3 g1 = (tA + tB + tF + tG) * 0.25;
|
|
float3 g2 = (tB + tC + tG + tH) * 0.25;
|
|
float3 g3 = (tF + tG + tK + tL) * 0.25;
|
|
float3 g4 = (tG + tH + tL + tM) * 0.25;
|
|
#ifdef BLOOM_FIRST
|
|
float w0 = 0.5 / (1.0 + dot(g0, float3(0.2126, 0.7152, 0.0722)));
|
|
float w1 = 0.125 / (1.0 + dot(g1, float3(0.2126, 0.7152, 0.0722)));
|
|
float w2 = 0.125 / (1.0 + dot(g2, float3(0.2126, 0.7152, 0.0722)));
|
|
float w3 = 0.125 / (1.0 + dot(g3, float3(0.2126, 0.7152, 0.0722)));
|
|
float w4 = 0.125 / (1.0 + dot(g4, float3(0.2126, 0.7152, 0.0722)));
|
|
float3 c = (g0 * w0 + g1 * w1 + g2 * w2 + g3 * w3 + g4 * w4) /
|
|
(w0 + w1 + w2 + w3 + w4);
|
|
return float4(SoftThreshold(max(c, 0.0)), 1.0);
|
|
#else
|
|
return float4(g0 * 0.5 + (g1 + g2 + g3 + g4) * 0.125, 1.0);
|
|
#endif
|
|
}
|
|
|
|
// 3x3 tent upsample of the next-smaller level; the PSO blends it additively
|
|
// onto the destination level (progressive upsampling accumulates every
|
|
// pyramid level's contribution with widening support).
|
|
float4 ps_bloom_up(VSOut i) : SV_Target {
|
|
float2 ts = src.zw;
|
|
float3 c = tex0.SampleLevel(smp_linear, i.uv + ts * float2(-1, -1), 0).rgb;
|
|
c += tex0.SampleLevel(smp_linear, i.uv + ts * float2(0, -1), 0).rgb * 2.0;
|
|
c += tex0.SampleLevel(smp_linear, i.uv + ts * float2(1, -1), 0).rgb;
|
|
c += tex0.SampleLevel(smp_linear, i.uv + ts * float2(-1, 0), 0).rgb * 2.0;
|
|
c += tex0.SampleLevel(smp_linear, i.uv, 0).rgb * 4.0;
|
|
c += tex0.SampleLevel(smp_linear, i.uv + ts * float2(1, 0), 0).rgb * 2.0;
|
|
c += tex0.SampleLevel(smp_linear, i.uv + ts * float2(-1, 1), 0).rgb;
|
|
c += tex0.SampleLevel(smp_linear, i.uv + ts * float2(0, 1), 0).rgb * 2.0;
|
|
c += tex0.SampleLevel(smp_linear, i.uv + ts * float2(1, 1), 0).rgb;
|
|
return float4(c * (1.0 / 16.0), 1.0);
|
|
}
|
|
|
|
// Final tonemap into the gamma guest output. tex0 = the full-res scene xe
|
|
// plane (point-sampled, 1:1), tex1 = bloom pyramid level 0 (bilinear
|
|
// upsample; the white fallback rides here with intensity 0 when bloom is
|
|
// off).
|
|
float4 ps_tonemap(VSOut i) : SV_Target {
|
|
// Graphics build-up showcase (see scene.hlsl ShowcaseMask): sh_v2.y/z =
|
|
// 256 + the layer mask left/right of the split at sh_v2.w (output
|
|
// pixels), 0 = off. Post layers reveal per pixel by their mask bits:
|
|
// AO 16, volumetrics 64, bloom 128 (SSR gates in its own composite).
|
|
// Compiled in only for the SHOWCASE=1 variant; the default build folds
|
|
// the mask to off and every gate below dead-strips.
|
|
#ifndef SHOWCASE
|
|
#define SHOWCASE 0
|
|
#endif
|
|
#if SHOWCASE
|
|
float scv = i.pos.x < sh_v2.w ? sh_v2.y : sh_v2.z;
|
|
int sc_mask = scv < 255.5 ? -1 : (int)(scv + 0.5) - 256;
|
|
#else
|
|
const int sc_mask = -1;
|
|
#endif
|
|
float ao = ao_plane.SampleLevel(smp_linear, i.uv, 0);
|
|
if (sc_mask >= 0 && (sc_mask & 16) == 0) {
|
|
ao = 1.0;
|
|
}
|
|
float3 x = max(tex0.SampleLevel(smp_point, i.uv, 0).rgb, 0.0) * ao;
|
|
float3 bloom = tex1.SampleLevel(smp_linear, i.uv, 0).rgb * p0.z;
|
|
if (sc_mask >= 0 && (sc_mask & 128) == 0) {
|
|
bloom = 0.0;
|
|
}
|
|
// Volumetric terms, applied AFTER the AO multiply (in-air light is not
|
|
// subject to surface occlusion). The shaft plane carries the marched
|
|
// SHADOWED-air term (see ps_vol_shafts) and dims MULTIPLICATIVELY;
|
|
// shadowed air removes a proportional share of the light seen through
|
|
// it. Against the bright sky that reads as dark crepuscular shafts;
|
|
// against a dim facade it is a slight deepening; fully lit air changes
|
|
// nothing. An ABSOLUTE subtraction was tried and rejected: the game
|
|
// bakes its scatter glow into bright sky pixels only, so subtracting a
|
|
// phase-weighted constant clamped whole shaded street canyons toward
|
|
// black. The half-res plane upsamples depth-aware (weights collapse
|
|
// across depth discontinuities) so shaft edges never bleed onto near
|
|
// silhouettes.
|
|
float shaft = 0.0;
|
|
bool sc_vol = sc_mask < 0 || (sc_mask & 64) != 0;
|
|
if (vs0.w > 0.0 && sc_vol) {
|
|
float dpix = lin_depth.SampleLevel(smp_point, i.uv, 0);
|
|
// One shaft-plane texel in uv (vs0.x = the march resolution divisor).
|
|
float2 vt = size.zw * max(vs0.x, 2.0);
|
|
float2 base = (floor(i.uv / vt - 0.5) + 0.5) * vt;
|
|
float2 f = saturate((i.uv - base) / vt);
|
|
float acc = 0.0;
|
|
float wsum = 0.0;
|
|
[unroll] for (int k = 0; k < 4; ++k) {
|
|
const float2 kC[4] = {float2(0.0, 0.0), float2(1.0, 0.0),
|
|
float2(0.0, 1.0), float2(1.0, 1.0)};
|
|
float2 su = base + kC[k] * vt;
|
|
float w = (kC[k].x > 0.5 ? f.x : 1.0 - f.x) *
|
|
(kC[k].y > 0.5 ? f.y : 1.0 - f.y);
|
|
float ds = lin_depth.SampleLevel(smp_point, su, 0);
|
|
w *= 1.0 / (1.0 + 8.0 * abs(ds - dpix) / max(dpix, 1.0));
|
|
acc += vol_plane.SampleLevel(smp_point, su, 0).r * w;
|
|
wsum += w;
|
|
}
|
|
shaft = acc / max(wsum, 1e-4) * vs0.w;
|
|
}
|
|
// Directional haze: the phase function's excess over its perpendicular
|
|
// value only; the game's own fog already renders the isotropic part,
|
|
// so adding a floor here just veils the whole frame.
|
|
float3 haze = float3(0.0, 0.0, 0.0);
|
|
if (vs1.w > 0.0 && sc_vol) {
|
|
float d = lin_depth.SampleLevel(smp_point, i.uv, 0);
|
|
float path = 1.0 - exp(-max(vs2.w, 0.0005) * d);
|
|
float3 ray = normalize(ViewPos(i.uv, 1.0));
|
|
float cosv = dot(ray, vs2.xyz);
|
|
const float g = 0.75;
|
|
float denom = max(1.0 - g * cosv, 1e-3);
|
|
float phase = (1.0 - g * g) / (denom * denom);
|
|
phase = min(max(phase - (1.0 - g * g), 0.0), 3.0);
|
|
haze = vs1.xyz * (phase * path * vs1.w);
|
|
}
|
|
if (p0.w > 4.5) {
|
|
return float4(ToneMapScene(haze), 1.0); // debug: haze term only
|
|
}
|
|
if (p0.w > 3.5) {
|
|
// Debug: the shaft shadowing term only (inverted feel: bright = air
|
|
// that will darken in the composite).
|
|
return float4(ToneMapScene(shaft.xxx), 1.0);
|
|
}
|
|
if (p0.w > 2.5) {
|
|
return float4(ao, ao, ao, 1.0); // debug: AO multiplier plane
|
|
}
|
|
if (p0.w > 1.5) {
|
|
return float4(saturate(x), 1.0); // debug: raw xe
|
|
}
|
|
if (p0.w > 0.5) {
|
|
return float4(ToneMapScene(bloom), 1.0); // debug: bloom term only
|
|
}
|
|
// Dimming ceiling: shadowed air thins the light, it does not extinguish
|
|
// it; without the ceiling a fully shaded sun-facing view (under an
|
|
// overpass) saturated to a solid black void.
|
|
float3 outc =
|
|
ToneMapScene((x + haze) * (1.0 - 0.55 * saturate(shaft)) + bloom);
|
|
#if SHOWCASE
|
|
// Showcase divider: a soft white line at the split while a wipe is in
|
|
// flight (the two sides show different stages).
|
|
if (sh_v2.w > 0.5 && abs(sh_v2.y - sh_v2.z) > 0.5) {
|
|
float half_w = max(size.x * 0.0012, 1.5);
|
|
float d = abs(i.pos.x - sh_v2.w);
|
|
outc = lerp(outc, float3(1.0, 1.0, 1.0),
|
|
saturate(1.0 - d / half_w) * 0.85);
|
|
}
|
|
#endif
|
|
return float4(outc, 1.0);
|
|
}
|