render: volumetric sun shafts, directional haze, and a movable-sun lighting lab
This commit is contained in:
parent
ee09d1655a
commit
f2aaf4081e
7 changed files with 1005 additions and 16 deletions
|
|
@ -15,23 +15,92 @@
|
||||||
// (anti-firefly) on the mip-0 extraction
|
// (anti-firefly) on the mip-0 extraction
|
||||||
// ps_bloom_up 3x3 tent upsample, additive-blended onto the next-larger
|
// ps_bloom_up 3x3 tent upsample, additive-blended onto the next-larger
|
||||||
// level (ONE/ONE at the PSO)
|
// level (ONE/ONE at the PSO)
|
||||||
// ps_tonemap scene xe + bloom -> the exact classic tone chain -> the
|
// ps_tonemap scene xe + bloom + volumetric terms -> the exact classic
|
||||||
// gamma guest output
|
// 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 static world-shadow map, 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) {
|
cbuffer C : register(b0) {
|
||||||
float4 size; // xy = destination size in px, zw = 1 / destination size
|
float4 size; // xy = destination size in px, zw = 1 / destination size
|
||||||
float4 src; // xy = source size in px, zw = 1 / source size
|
float4 src; // xy = source size in px, zw = 1 / source size
|
||||||
float4 p0; // x = bloom threshold (xe space), y = soft knee,
|
float4 p0; // x = bloom threshold (xe space), y = soft knee,
|
||||||
// z = bloom intensity, w = debug (1 = bloom term, 2 = raw xe)
|
// 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
|
float4 p1; // x = downsample tap spread in source texels (2 on the 4x
|
||||||
// extraction so the 13-tap footprint covers the whole
|
// extraction so the 13-tap footprint covers the whole
|
||||||
// source block, 1 on the 2x chain), yzw spare
|
// 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: w = shaft intensity (xyz 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 constant slice (the scene pass's b1; see scene.hlsl for
|
||||||
|
// the authoritative layout); ps_vol_shafts samples sun visibility with the
|
||||||
|
// exact receiver math the materials use. Only the rows up to the static
|
||||||
|
// world-shadow transform are declared (the tail is water state).
|
||||||
|
cbuffer S : register(b1) {
|
||||||
|
float4 sh_x; // light-space X row (xyz) + translation (w)
|
||||||
|
float4 sh_y; // light-space Y row
|
||||||
|
float4 sh_z; // depth row (height ramp)
|
||||||
|
float4 sh_c1; // cascade 1 scale.xy + offset.zw
|
||||||
|
float4 sh_c2; // cascade 2 scale.xy + offset.zw
|
||||||
|
float4 sh_color; // shadow color rgb + luma
|
||||||
|
float4 sh_misc; // x = depth bias, y = enable, zw = atlas dimensions
|
||||||
|
float4 sh_sun; // xyz = sun direction (toward the sun), w = exposure
|
||||||
|
float4 sh_env;
|
||||||
|
float4 sh_fogp;
|
||||||
|
float4 sh_fogc;
|
||||||
|
float4 dyn_sun;
|
||||||
|
float4 dyn_amb;
|
||||||
|
float4 dyn_misc; // y = static world-shadow floor
|
||||||
|
float4 sh_char;
|
||||||
|
float4 sh_v2;
|
||||||
|
float4 dyn_wsx; // static world-shadow rows: u = dot(wp4, wsx)*0.5+0.5,
|
||||||
|
float4 dyn_wsy; // v = dot(wp4, wsy)*-0.5+0.5,
|
||||||
|
float4 dyn_wsz; // ray depth = dot(wp4, wsz)
|
||||||
};
|
};
|
||||||
Texture2D<float4> tex0 : register(t0); // per-pass primary input
|
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<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
|
// ps_tonemap: the SSAO multiplier plane (fused pre-tonemap apply, replaces
|
||||||
// the classic path's separate full-res composite draw; white when AO is
|
// the classic path's separate full-res composite draw; white when AO is
|
||||||
// off). Bilinear upsample from the AO raster, like the classic composite.
|
// off). Bilinear upsample from the AO raster, like the classic composite.
|
||||||
Texture2D<float> ao_plane : register(t2);
|
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);
|
||||||
SamplerState smp_point : register(s0);
|
SamplerState smp_point : register(s0);
|
||||||
SamplerState smp_linear : register(s1);
|
SamplerState smp_linear : register(s1);
|
||||||
|
|
||||||
|
|
@ -55,6 +124,134 @@ float3 ToneMapScene(float3 x) {
|
||||||
return saturate(sqrt(max(tm * 0.5, 0.0)) * 1.41);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 and the static world-shadow map, 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);
|
||||||
|
float steps = clamp(p0.x, 8.0, 64.0);
|
||||||
|
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);
|
||||||
|
float wv = WorldVis(sp);
|
||||||
|
if (cv >= 0.0 || wv >= 0.0) {
|
||||||
|
lit += min(cv < 0.0 ? 1.0 : cv, wv < 0.0 ? 1.0 : wv);
|
||||||
|
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
|
// Soft-knee brightpass (kept in xe space, where 1.0 is the tonemapper's
|
||||||
// saturation point): quadratic ramp from (threshold - knee) to
|
// saturation point): quadratic ramp from (threshold - knee) to
|
||||||
// (threshold + knee), linear excess beyond.
|
// (threshold + knee), linear excess beyond.
|
||||||
|
|
@ -130,6 +327,62 @@ float4 ps_tonemap(VSOut i) : SV_Target {
|
||||||
float ao = ao_plane.SampleLevel(smp_linear, i.uv, 0);
|
float ao = ao_plane.SampleLevel(smp_linear, i.uv, 0);
|
||||||
float3 x = max(tex0.SampleLevel(smp_point, i.uv, 0).rgb, 0.0) * ao;
|
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;
|
float3 bloom = tex1.SampleLevel(smp_linear, i.uv, 0).rgb * p0.z;
|
||||||
|
// 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;
|
||||||
|
if (vs0.w > 0.0) {
|
||||||
|
float dpix = lin_depth.SampleLevel(smp_point, i.uv, 0);
|
||||||
|
float2 vt = size.zw * 2.0; // one shaft-plane texel in uv
|
||||||
|
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) {
|
||||||
|
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) {
|
if (p0.w > 2.5) {
|
||||||
return float4(ao, ao, ao, 1.0); // debug: AO multiplier plane
|
return float4(ao, ao, ao, 1.0); // debug: AO multiplier plane
|
||||||
}
|
}
|
||||||
|
|
@ -139,5 +392,9 @@ float4 ps_tonemap(VSOut i) : SV_Target {
|
||||||
if (p0.w > 0.5) {
|
if (p0.w > 0.5) {
|
||||||
return float4(ToneMapScene(bloom), 1.0); // debug: bloom term only
|
return float4(ToneMapScene(bloom), 1.0); // debug: bloom term only
|
||||||
}
|
}
|
||||||
return float4(ToneMapScene(x + bloom), 1.0);
|
// 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.
|
||||||
|
return float4(
|
||||||
|
ToneMapScene((x + haze) * (1.0 - 0.55 * saturate(shaft)) + bloom), 1.0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -217,8 +217,80 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_hdr_packed, true, "Skate 3",
|
||||||
REXCVAR_DEFINE_INT32(skate3_native_render_scene_hdr_debug, 0, "Skate 3",
|
REXCVAR_DEFINE_INT32(skate3_native_render_scene_hdr_debug, 0, "Skate 3",
|
||||||
"HDR post debug: 0 = off, 1 = show the bloom term only, 2 = show "
|
"HDR post debug: 0 = off, 1 = show the bloom term only, 2 = show "
|
||||||
"the raw pre-tonemap scene (clamped), 3 = show the fused SSAO "
|
"the raw pre-tonemap scene (clamped), 3 = show the fused SSAO "
|
||||||
"multiplier plane.")
|
"multiplier plane, 4 = show the sun-shaft plane only, 5 = show "
|
||||||
.range(0, 3)
|
"the directional haze term only.")
|
||||||
|
.range(0, 5)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_shafts, true, "Skate 3",
|
||||||
|
"Volumetric sun shafts: the air along each view ray is marched "
|
||||||
|
"against the dynamic CSM atlas and the static world-shadow map, "
|
||||||
|
"and the SHADOWED portion proportionally dims the light seen "
|
||||||
|
"through it: dark crepuscular shafts cut by real shadow "
|
||||||
|
"volumes (buildings, trees, underpasses) into the game's own "
|
||||||
|
"sun glow. Fully lit air is untouched, so open scenes stay "
|
||||||
|
"exactly as authored. Requires the HDR intermediate and this "
|
||||||
|
"frame's shadow pass.")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_shafts_intensity, 0.6,
|
||||||
|
"Skate 3",
|
||||||
|
"Sun-shaft strength: how strongly fully shadowed air dims "
|
||||||
|
"the scene behind it, scaled by the forward-scatter phase "
|
||||||
|
"(at the default, fully shaded air dims ~30-50% at typical "
|
||||||
|
"sun-facing angles and saturates looking straight at the "
|
||||||
|
"sun; 0 = off).")
|
||||||
|
.range(0.0, 1.5)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_shafts_reach, 40.0, "Skate 3",
|
||||||
|
"Sun-shaft march reach (world units): how far in front of "
|
||||||
|
"the camera the air is sampled for sun visibility. Longer "
|
||||||
|
"reaches pick up more distant shadow volumes at coarser "
|
||||||
|
"sampling for the same step count.")
|
||||||
|
.range(5.0, 300.0)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_INT32(skate3_native_render_scene_shafts_steps, 64, "Skate 3",
|
||||||
|
"Sun-shaft march steps per pixel (half-res pass). More steps "
|
||||||
|
"resolve thinner shadow volumes at proportional GPU cost.")
|
||||||
|
.range(8, 64)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_sun_override, false, "Skate 3",
|
||||||
|
"Lighting lab: replace the captured sun direction with the "
|
||||||
|
"azimuth/elevation cvars. Every per-frame light transform is "
|
||||||
|
"rebuilt around the new direction, so the dynamic CSM shadows, "
|
||||||
|
"the static world-shadow map, material shadow receivers and "
|
||||||
|
"the volumetric shafts all follow the moved sun. Baked "
|
||||||
|
"lightmap shade and the sky dome's painted sun cannot move "
|
||||||
|
"(game content), a tuning tool, not a time-of-day system.")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_sun_azimuth, 220.0, "Skate 3",
|
||||||
|
"Overridden sun azimuth in degrees (compass heading the sun "
|
||||||
|
"sits toward; 0 = +Z north, 90 = +X east).")
|
||||||
|
.range(0.0, 360.0)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_sun_elevation, 35.0, "Skate 3",
|
||||||
|
"Overridden sun elevation in degrees above the horizon (low "
|
||||||
|
"values = long shadows and the most visible shafts).")
|
||||||
|
.range(2.0, 88.0)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_haze, true, "Skate 3",
|
||||||
|
"Directional atmospheric haze: sun-scattered light added with "
|
||||||
|
"view distance, strongest looking toward the sun, tinted by the "
|
||||||
|
"frame's own fog color (so it tracks time of day and fades at "
|
||||||
|
"night). Kept subtle; the game's authored sky and fog carry "
|
||||||
|
"the base atmosphere, and additive terms read as extra fog "
|
||||||
|
"quickly. Requires the HDR intermediate.")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_haze_intensity, 0.08,
|
||||||
|
"Skate 3",
|
||||||
|
"Directional haze strength (pre-tonemap scale of the "
|
||||||
|
"toward-the-sun scattering excess; 0 = off).")
|
||||||
|
.range(0.0, 2.0)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_haze_density, 0.005,
|
||||||
|
"Skate 3",
|
||||||
|
"Directional haze density per view unit: how quickly the "
|
||||||
|
"scattering saturates with distance (0.005 reaches ~63% at "
|
||||||
|
"200 units).")
|
||||||
|
.range(0.0, 0.05)
|
||||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_2d, true, "Skate 3",
|
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_2d, true, "Skate 3",
|
||||||
"Replay the game's 2D/APT (Flash HUD) draws as a native overlay pass "
|
"Replay the game's 2D/APT (Flash HUD) draws as a native overlay pass "
|
||||||
|
|
@ -6948,6 +7020,105 @@ void PublishSplineDraws(uint8_t* base) {
|
||||||
g_scene_spline = std::move(published);
|
g_scene_spline = std::move(published);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GetCapturedSunDir(float out[3]) {
|
||||||
|
out[0] = g_sun_captured[0].load(std::memory_order_relaxed);
|
||||||
|
out[1] = g_sun_captured[1].load(std::memory_order_relaxed);
|
||||||
|
out[2] = g_sun_captured[2].load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild a world->light affine row (xyz = axis * scale, w = translation)
|
||||||
|
// around a new axis, preserving the row's scale and its value at the camera
|
||||||
|
// - cascade centering and bias conventions carry over unchanged.
|
||||||
|
static void RebasisLightRow(float* row, const float axis[3],
|
||||||
|
const float cam[3]) {
|
||||||
|
const float scale = std::sqrt(row[0] * row[0] + row[1] * row[1] +
|
||||||
|
row[2] * row[2]);
|
||||||
|
if (scale < 1e-8f) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float at_cam =
|
||||||
|
row[0] * cam[0] + row[1] * cam[1] + row[2] * cam[2] + row[3];
|
||||||
|
row[0] = axis[0] * scale;
|
||||||
|
row[1] = axis[1] * scale;
|
||||||
|
row[2] = axis[2] * scale;
|
||||||
|
row[3] = at_cam - (row[0] * cam[0] + row[1] * cam[1] + row[2] * cam[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lighting-lab sun override (skate3_native_render_scene_sun_override):
|
||||||
|
// rotates every captured per-frame light transform in the published scene
|
||||||
|
// to the azimuth/elevation-driven direction. The native CSM caster pass,
|
||||||
|
// the material receivers, the static world-shadow map (which re-primes
|
||||||
|
// automatically when its rows change) and the volumetric shafts all read
|
||||||
|
// these rows, so the whole dynamic lighting stack follows the moved sun.
|
||||||
|
// Baked lightmap shade and the sky dome's painted sun are game content and
|
||||||
|
// stay put.
|
||||||
|
static void ApplySunOverride(FrameScene& scene) {
|
||||||
|
if (!REXCVAR_GET(skate3_native_render_scene_sun_override)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float kDeg = 0.01745329252f;
|
||||||
|
const float az = float(REXCVAR_GET(skate3_native_render_scene_sun_azimuth)) * kDeg;
|
||||||
|
const float el =
|
||||||
|
float(REXCVAR_GET(skate3_native_render_scene_sun_elevation)) * kDeg;
|
||||||
|
// Unit vector TOWARD the sun (y up; azimuth 0 = +Z, 90 = +X).
|
||||||
|
const float sun[3] = {std::cos(el) * std::sin(az), std::sin(el),
|
||||||
|
std::cos(el) * std::cos(az)};
|
||||||
|
// Light basis: depth axis points away from the sun; X/Y span the shadow
|
||||||
|
// plane. Handedness is irrelevant; casters and receivers share the rows.
|
||||||
|
const float zl[3] = {-sun[0], -sun[1], -sun[2]};
|
||||||
|
float xl[3] = {zl[2], 0.0f, -zl[0]}; // cross((0,1,0), zl)
|
||||||
|
const float xll = std::sqrt(xl[0] * xl[0] + xl[2] * xl[2]);
|
||||||
|
if (xll > 1e-5f) {
|
||||||
|
xl[0] /= xll;
|
||||||
|
xl[2] /= xll;
|
||||||
|
} else {
|
||||||
|
xl[0] = 1.0f;
|
||||||
|
xl[2] = 0.0f;
|
||||||
|
}
|
||||||
|
const float yl[3] = {zl[1] * xl[2] - zl[2] * xl[1],
|
||||||
|
zl[2] * xl[0] - zl[0] * xl[2],
|
||||||
|
zl[0] * xl[1] - zl[1] * xl[0]};
|
||||||
|
const float* cam = scene.cam_pos;
|
||||||
|
if (scene.shadow_valid) {
|
||||||
|
RebasisLightRow(scene.shadow_rows + 0, xl, cam); // c0 light-space X
|
||||||
|
RebasisLightRow(scene.shadow_rows + 12, yl, cam); // c3 light-space Y
|
||||||
|
RebasisLightRow(scene.shadow_rows + 16, zl, cam); // c4 depth ramp
|
||||||
|
scene.shadow_rows[24] = sun[0]; // c6 sun direction
|
||||||
|
scene.shadow_rows[25] = sun[1];
|
||||||
|
scene.shadow_rows[26] = sun[2];
|
||||||
|
// Re-center the cascade sub-boxes (c1/c2 scale.xy + offset.zw) on the
|
||||||
|
// camera: the game fits their offsets for ITS sun each frame, and
|
||||||
|
// under a rotated basis the finest cascade can drift off the player;
|
||||||
|
// the shadow then falls back to the coarser tiles (visibly blocky).
|
||||||
|
// Caster pass and receivers share these rows, so the re-fit stays
|
||||||
|
// self-consistent.
|
||||||
|
const float* rows = scene.shadow_rows;
|
||||||
|
const float lsx =
|
||||||
|
rows[0] * cam[0] + rows[1] * cam[1] + rows[2] * cam[2] + rows[3];
|
||||||
|
const float lsy =
|
||||||
|
rows[12] * cam[0] + rows[13] * cam[1] + rows[14] * cam[2] + rows[15];
|
||||||
|
scene.shadow_rows[6] = -lsx * scene.shadow_rows[4]; // c1 offset
|
||||||
|
scene.shadow_rows[7] = -lsy * scene.shadow_rows[5];
|
||||||
|
scene.shadow_rows[10] = -lsx * scene.shadow_rows[8]; // c2 offset
|
||||||
|
scene.shadow_rows[11] = -lsy * scene.shadow_rows[9];
|
||||||
|
}
|
||||||
|
if (scene.dynobj_valid) {
|
||||||
|
scene.dynobj_rows[0] = sun[0];
|
||||||
|
scene.dynobj_rows[1] = sun[1];
|
||||||
|
scene.dynobj_rows[2] = sun[2];
|
||||||
|
}
|
||||||
|
if (scene.dynobj_ws_valid) {
|
||||||
|
RebasisLightRow(scene.dynobj_ws + 0, xl, cam);
|
||||||
|
RebasisLightRow(scene.dynobj_ws + 4, yl, cam);
|
||||||
|
RebasisLightRow(scene.dynobj_ws + 8, zl, cam);
|
||||||
|
}
|
||||||
|
if (scene.sky_sun_valid) {
|
||||||
|
scene.sky_sun[0] = sun[0];
|
||||||
|
scene.sky_sun[1] = sun[1];
|
||||||
|
scene.sky_sun[2] = sun[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
if (!SceneEnabled()) {
|
if (!SceneEnabled()) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -8570,6 +8741,18 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
std::memcpy(scene.sky_sun, g_sky_sun, sizeof(g_sky_sun));
|
std::memcpy(scene.sky_sun, g_sky_sun, sizeof(g_sky_sun));
|
||||||
scene.sky_sun_valid = true;
|
scene.sky_sun_valid = true;
|
||||||
}
|
}
|
||||||
|
// Publish the captured sun direction BEFORE any override (the debug
|
||||||
|
// dialog seeds the override sliders from it).
|
||||||
|
{
|
||||||
|
const float* cap = scene.shadow_valid ? scene.shadow_rows + 24
|
||||||
|
: scene.sky_sun;
|
||||||
|
if (scene.shadow_valid || scene.sky_sun_valid) {
|
||||||
|
g_sun_captured[0].store(cap[0], std::memory_order_relaxed);
|
||||||
|
g_sun_captured[1].store(cap[1], std::memory_order_relaxed);
|
||||||
|
g_sun_captured[2].store(cap[2], std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ApplySunOverride(scene);
|
||||||
// The game's own popup blur chain (pause menu etc.), untouched by the
|
// The game's own popup blur chain (pause menu etc.), untouched by the
|
||||||
// host settings menu, whose gaussian reads host state directly at render
|
// host settings menu, whose gaussian reads host state directly at render
|
||||||
// time (see ApplyMenuBlurPass call sites).
|
// time (see ApplyMenuBlurPass call sites).
|
||||||
|
|
|
||||||
|
|
@ -428,6 +428,10 @@ bool Enabled();
|
||||||
// its dark tint on top of the blurred scene.
|
// its dark tint on top of the blurred scene.
|
||||||
void SetSettingsMenuBlur(bool enabled);
|
void SetSettingsMenuBlur(bool enabled);
|
||||||
|
|
||||||
|
// The captured (un-overridden) sun direction, for the debug dialog's sun
|
||||||
|
// override to seed its sliders from (unit vector toward the sun).
|
||||||
|
void GetCapturedSunDir(float out[3]);
|
||||||
|
|
||||||
// The game's per-entity spawn/streaming fade as carried by the item's
|
// The game's per-entity spawn/streaming fade as carried by the item's
|
||||||
// validated character-lighting capture (peds c21.x, defaultcharacter c13.x,
|
// validated character-lighting capture (peds c21.x, defaultcharacter c13.x,
|
||||||
// cacstamp c22.x, vehicle body c20.x, hair strand scale). 1.0 for items
|
// cacstamp c22.x, vehicle body c20.x, hair strand scale). 1.0 for items
|
||||||
|
|
|
||||||
|
|
@ -8586,13 +8586,25 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
post_ran = true;
|
post_ran = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Volumetric lighting (hdr.hlsl ps_vol_*) ----
|
||||||
|
// Shadow-marched sun shafts (per-step visibility against the CSM atlas +
|
||||||
|
// the static world-shadow map), plus the constant staging for the
|
||||||
|
// directional haze; both terms join in ps_tonemap after the AO multiply,
|
||||||
|
// so they bloom and tonemap like scene light.
|
||||||
|
if (hdr_on && use_depth && !loading_native &&
|
||||||
|
ApplyVolumetricPass(context, cmd, scene, viewport, scissor, ssao_ran,
|
||||||
|
frame_number)) {
|
||||||
|
post_ran = true;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- HDR post (hdr.hlsl: bloom pyramid + the extracted tonemap) ----
|
// ---- HDR post (hdr.hlsl: bloom pyramid + the extracted tonemap) ----
|
||||||
// Bloom reads the AO-composited float scene; ps_tonemap applies the
|
// Bloom reads the AO-composited float scene; ps_tonemap applies the
|
||||||
// game's shared tone chain once into the guest output, which every later
|
// game's shared tone chain once into the guest output, which every later
|
||||||
// consumer (photo chain, 2D overlay, blur, grab) reads exactly as on the
|
// consumer (photo chain, 2D overlay, blur, grab) reads exactly as on the
|
||||||
// classic path.
|
// classic path.
|
||||||
if (hdr_on) {
|
if (hdr_on) {
|
||||||
ApplyHdrPost(context, cmd, viewport, scissor, loading_native);
|
ApplyHdrPost(context, cmd, viewport, scissor, loading_native,
|
||||||
|
frame_number);
|
||||||
post_ran = true;
|
post_ran = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -669,6 +669,45 @@ struct RendererState {
|
||||||
// Per-frame handoff: the G-buffer pass drew and left ssr_gbuf in
|
// Per-frame handoff: the G-buffer pass drew and left ssr_gbuf in
|
||||||
// PIXEL_SHADER_RESOURCE for ApplySsrPass (which consumes + restores it).
|
// PIXEL_SHADER_RESOURCE for ApplySsrPass (which consumes + restores it).
|
||||||
bool ssr_gbuf_ready = false;
|
bool ssr_gbuf_ready = false;
|
||||||
|
// Volumetric lighting (hdr.hlsl ps_vol_*): shadow-marched sun shafts,
|
||||||
|
// a half-res world-space march testing per-step sun visibility against
|
||||||
|
// the CSM atlas + the static world-shadow map (real shadowed air; no
|
||||||
|
// screen-space silhouette dependence), plus an analytic directional
|
||||||
|
// haze, both fused into ps_tonemap pre-tonemap (after the AO multiply;
|
||||||
|
// in-air light is not surface-occluded). HDR path only; shares the HDR
|
||||||
|
// binding layout (which carries the shadow constant slice at b1). The
|
||||||
|
// shaft plane idles in RENDER_TARGET state.
|
||||||
|
nrhi::Pipeline* pso_vol_linearize = nullptr; // fallback when SSAO idle
|
||||||
|
nrhi::Pipeline* pso_vol_shafts = nullptr;
|
||||||
|
// 3x3 tent over the marched plane (the ps_bloom_up shader without the
|
||||||
|
// additive blend): integrates the march's per-pixel jitter dither and
|
||||||
|
// softens shadow-volume edges.
|
||||||
|
nrhi::Pipeline* pso_vol_blur = nullptr;
|
||||||
|
nrhi::Texture* vol_tex = nullptr; // half res RGBA16F march output
|
||||||
|
nrhi::Texture* vol_tex_b = nullptr; // blurred plane (the tonemap input)
|
||||||
|
nrhi::TextureView* vol_srv = nullptr;
|
||||||
|
nrhi::TextureView* vol_srv_b = nullptr;
|
||||||
|
uint32_t vol_width = 0, vol_height = 0;
|
||||||
|
nrhi::Texture* vol_lin_depth = nullptr; // full res R32F (own linearize)
|
||||||
|
nrhi::TextureView* vol_lin_srv = nullptr;
|
||||||
|
uint32_t vol_lin_width = 0, vol_lin_height = 0;
|
||||||
|
// Scene-depth SRV for the own linearize, re-pointed on depth rebuilds.
|
||||||
|
nrhi::TextureView* vol_depth_srv = nullptr;
|
||||||
|
nrhi::Texture* vol_depth_srv_of = nullptr;
|
||||||
|
uint32_t vol_msaa = 0; // linearize-variant PSO latch
|
||||||
|
bool vol_failed = false;
|
||||||
|
// Per-frame handoff to ApplyHdrPost: the finished shaft plane and the
|
||||||
|
// linear-depth plane used this frame (the SSAO plane or the own
|
||||||
|
// linearize) are left in PIXEL_SHADER_RESOURCE for ps_tonemap (t3/t4);
|
||||||
|
// ApplyHdrPost restores both and clears the flags.
|
||||||
|
bool vol_plane_in_psr = false;
|
||||||
|
nrhi::Texture* vol_lin_plane = nullptr;
|
||||||
|
nrhi::TextureView* vol_lin_plane_srv = nullptr;
|
||||||
|
bool vol_lin_in_psr = false;
|
||||||
|
// ps_tonemap's volumetric constant rows (b0 rows vp/vs0/vs1/vs2 tail),
|
||||||
|
// staged by ApplyVolumetricPass; zeros disable every term.
|
||||||
|
bool vol_tonemap_valid = false;
|
||||||
|
float vol_rows[16] = {};
|
||||||
std::unordered_map<uint32_t, MeshBuffers> meshes;
|
std::unordered_map<uint32_t, MeshBuffers> meshes;
|
||||||
// (The old D3D12 bookkeeping, the retired-resource vector, the CPU SRV
|
// (The old D3D12 bookkeeping, the retired-resource vector, the CPU SRV
|
||||||
// staging heap and its slot allocator/recycling lists, is gone: resource
|
// staging heap and its slot allocator/recycling lists, is gone: resource
|
||||||
|
|
@ -885,9 +924,16 @@ bool ApplySsrPass(const NativeGuestOutputRenderContext& context,
|
||||||
nrhi::Cmd* cmd, const FrameScene& scene,
|
nrhi::Cmd* cmd, const FrameScene& scene,
|
||||||
const nrhi::Viewport& viewport, const nrhi::Rect& scissor,
|
const nrhi::Viewport& viewport, const nrhi::Rect& scissor,
|
||||||
bool ssao_ran);
|
bool ssao_ran);
|
||||||
|
bool EnsureVolumetricPipeline(const NativeGuestOutputRenderContext& context);
|
||||||
|
bool ApplyVolumetricPass(const NativeGuestOutputRenderContext& context,
|
||||||
|
nrhi::Cmd* cmd, const FrameScene& scene,
|
||||||
|
const nrhi::Viewport& viewport,
|
||||||
|
const nrhi::Rect& scissor, bool ssao_ran,
|
||||||
|
uint64_t frame_number);
|
||||||
void ApplyHdrPost(const NativeGuestOutputRenderContext& context,
|
void ApplyHdrPost(const NativeGuestOutputRenderContext& context,
|
||||||
nrhi::Cmd* cmd, const nrhi::Viewport& viewport,
|
nrhi::Cmd* cmd, const nrhi::Viewport& viewport,
|
||||||
const nrhi::Rect& scissor, bool loading_native);
|
const nrhi::Rect& scissor, bool loading_native,
|
||||||
|
uint64_t frame_number);
|
||||||
bool ApplyMenuBlurPass(const NativeGuestOutputRenderContext& context, nrhi::Cmd* cmd,
|
bool ApplyMenuBlurPass(const NativeGuestOutputRenderContext& context, nrhi::Cmd* cmd,
|
||||||
float target_sigma, bool output_in_guest_output_state);
|
float target_sigma, bool output_in_guest_output_state);
|
||||||
// Blur-over-emulated-frames post processor registered by Install().
|
// Blur-over-emulated-frames post processor registered by Install().
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,16 @@ REXCVAR_DECLARE(bool, skate3_native_render_scene_bloom);
|
||||||
REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_intensity);
|
REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_intensity);
|
||||||
REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_knee);
|
REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_knee);
|
||||||
REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_threshold);
|
REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_threshold);
|
||||||
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_haze);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_haze_density);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_haze_intensity);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_hdr);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_hdr);
|
||||||
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_hdr_debug);
|
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_hdr_debug);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_hdr_packed);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_hdr_packed);
|
||||||
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_shafts);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_shafts_intensity);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_shafts_reach);
|
||||||
|
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_shafts_steps);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_ssao_debug);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_ssao_debug);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_ssao_full_res);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_ssao_full_res);
|
||||||
REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_intensity);
|
REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_intensity);
|
||||||
|
|
@ -845,6 +852,438 @@ bool ApplySsrPass(const NativeGuestOutputRenderContext& context,
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Volumetric lighting (hdr.hlsl ps_vol_*: sun shafts + haze) ---------
|
||||||
|
// PSOs under the HDR binding layout, built lazily on the first enabled frame
|
||||||
|
// and rebuilt when the MSAA level (linearize variant) changes. Failure
|
||||||
|
// disables the effect only.
|
||||||
|
bool EnsureVolumetricPipeline(const NativeGuestOutputRenderContext& context) {
|
||||||
|
if (g_r.vol_failed || !g_r.hdr_active || g_r.hdr_layout == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (g_r.pso_vol_shafts != nullptr && g_r.vol_msaa == g_r.msaa) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
nrhi::Device* device = context.device;
|
||||||
|
const auto fail = [&](const char* what) {
|
||||||
|
REXLOG_ERROR("native-scene: volumetric pipeline setup failed ({})", what);
|
||||||
|
g_r.vol_failed = true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
for (nrhi::Pipeline** p : {&g_r.pso_vol_linearize, &g_r.pso_vol_shafts,
|
||||||
|
&g_r.pso_vol_blur}) {
|
||||||
|
if (*p != nullptr) {
|
||||||
|
device->DestroyDeferred(*p);
|
||||||
|
*p = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bool msaa = g_r.msaa > 1;
|
||||||
|
const nrhi::ShaderMacro msaa_defs[] = {{"VOL_MSAA", "1"},
|
||||||
|
{nullptr, nullptr}};
|
||||||
|
nrhi::Shader* vs = device->CreateShader(
|
||||||
|
MakeShaderDesc(nrhi::ShaderStage::kVertex, "hdr.hlsl", kHdrShaderSource,
|
||||||
|
"vs_main", nullptr, ""));
|
||||||
|
nrhi::Shader* ps_lin = device->CreateShader(MakeShaderDesc(
|
||||||
|
nrhi::ShaderStage::kPixel, "hdr.hlsl", kHdrShaderSource,
|
||||||
|
"ps_vol_linearize", msaa ? msaa_defs : nullptr,
|
||||||
|
msaa ? "VOL_MSAA=1" : ""));
|
||||||
|
nrhi::Shader* ps_shafts = device->CreateShader(
|
||||||
|
MakeShaderDesc(nrhi::ShaderStage::kPixel, "hdr.hlsl", kHdrShaderSource,
|
||||||
|
"ps_vol_shafts", nullptr, ""));
|
||||||
|
// The 3x3 tent blur reuses ps_bloom_up (in a non-blended PSO): it
|
||||||
|
// integrates the march jitter and softens shadow-volume edges.
|
||||||
|
nrhi::Shader* ps_blur = device->CreateShader(
|
||||||
|
MakeShaderDesc(nrhi::ShaderStage::kPixel, "hdr.hlsl", kHdrShaderSource,
|
||||||
|
"ps_bloom_up", nullptr, ""));
|
||||||
|
const bool shaders_ok = vs != nullptr && ps_lin != nullptr &&
|
||||||
|
ps_shafts != nullptr && ps_blur != nullptr;
|
||||||
|
if (!shaders_ok) {
|
||||||
|
for (nrhi::Shader* s : {vs, ps_lin, ps_shafts, ps_blur}) {
|
||||||
|
device->DestroyDeferred(s);
|
||||||
|
}
|
||||||
|
return fail("shader compile");
|
||||||
|
}
|
||||||
|
nrhi::GraphicsPipelineDesc pso;
|
||||||
|
pso.layout = g_r.hdr_layout;
|
||||||
|
pso.vs = vs;
|
||||||
|
pso.cull = nrhi::CullMode::kNone;
|
||||||
|
pso.sample_count = 1;
|
||||||
|
pso.ps = ps_lin;
|
||||||
|
pso.rtv_format = nrhi::Format::kR32_FLOAT;
|
||||||
|
g_r.pso_vol_linearize = device->CreateGraphicsPipeline(pso);
|
||||||
|
pso.rtv_format = nrhi::Format::kR16G16B16A16_FLOAT;
|
||||||
|
pso.ps = ps_shafts;
|
||||||
|
g_r.pso_vol_shafts = device->CreateGraphicsPipeline(pso);
|
||||||
|
pso.ps = ps_blur;
|
||||||
|
g_r.pso_vol_blur = device->CreateGraphicsPipeline(pso);
|
||||||
|
for (nrhi::Shader* s : {vs, ps_lin, ps_shafts, ps_blur}) {
|
||||||
|
device->DestroyDeferred(s);
|
||||||
|
}
|
||||||
|
if (g_r.pso_vol_linearize == nullptr || g_r.pso_vol_shafts == nullptr ||
|
||||||
|
g_r.pso_vol_blur == nullptr) {
|
||||||
|
return fail("pso");
|
||||||
|
}
|
||||||
|
g_r.vol_msaa = g_r.msaa;
|
||||||
|
REXLOG_INFO("native-scene: volumetric pipeline created (MSAA x{})",
|
||||||
|
g_r.msaa);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// General 4x4 inverse (Gauss-Jordan). Returns false on a singular matrix.
|
||||||
|
static bool Invert4x4(const float m[16], float out[16]) {
|
||||||
|
float a[4][8];
|
||||||
|
for (int r = 0; r < 4; ++r) {
|
||||||
|
for (int c = 0; c < 4; ++c) {
|
||||||
|
a[r][c] = m[r * 4 + c];
|
||||||
|
a[r][c + 4] = r == c ? 1.0f : 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int col = 0; col < 4; ++col) {
|
||||||
|
int pivot = col;
|
||||||
|
for (int r = col + 1; r < 4; ++r) {
|
||||||
|
if (std::fabs(a[r][col]) > std::fabs(a[pivot][col])) {
|
||||||
|
pivot = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (std::fabs(a[pivot][col]) < 1e-12f) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (pivot != col) {
|
||||||
|
for (int c = 0; c < 8; ++c) {
|
||||||
|
std::swap(a[col][c], a[pivot][c]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const float inv = 1.0f / a[col][col];
|
||||||
|
for (int c = 0; c < 8; ++c) {
|
||||||
|
a[col][c] *= inv;
|
||||||
|
}
|
||||||
|
for (int r = 0; r < 4; ++r) {
|
||||||
|
if (r == col) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const float f = a[r][col];
|
||||||
|
for (int c = 0; c < 8; ++c) {
|
||||||
|
a[r][c] -= f * a[col][c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int r = 0; r < 4; ++r) {
|
||||||
|
for (int c = 0; c < 4; ++c) {
|
||||||
|
out[r * 4 + c] = a[r][c + 4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shadow-marched sun shafts + the constant staging for the haze term
|
||||||
|
// ps_tonemap evaluates. Runs between the SSR composite and the HDR post.
|
||||||
|
// Leaves the shaft plane and the linear-depth plane in
|
||||||
|
// PIXEL_SHADER_RESOURCE for ps_tonemap (ApplyHdrPost restores both).
|
||||||
|
// Returns true when it staged anything; the binding layout changed and
|
||||||
|
// the caller's post_ran block restores the main-pass bindings.
|
||||||
|
bool ApplyVolumetricPass(const NativeGuestOutputRenderContext& context,
|
||||||
|
nrhi::Cmd* cmd, const FrameScene& scene,
|
||||||
|
const nrhi::Viewport& viewport,
|
||||||
|
const nrhi::Rect& scissor, bool ssao_ran,
|
||||||
|
uint64_t frame_number) {
|
||||||
|
// Shafts need this frame's shadow state: the receiver constants (b1
|
||||||
|
// slice), the blurred CSM atlas left in PIXEL_SHADER_RESOURCE by the
|
||||||
|
// shadow pass, and a valid capture. The haze needs only the sun capture.
|
||||||
|
const bool shafts = REXCVAR_GET(skate3_native_render_scene_shafts) &&
|
||||||
|
scene.shadow_valid && g_r.shadow_cb != nullptr &&
|
||||||
|
g_r.shadow_srv_final != nullptr &&
|
||||||
|
g_r.shadow_in_srv_state;
|
||||||
|
const bool haze_want =
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_haze) && scene.sky_sun_valid;
|
||||||
|
if ((!shafts && !haze_want) || g_r.hdr_resolved == nullptr ||
|
||||||
|
g_r.hdr_srv == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// The published projection must be the live perspective matrix.
|
||||||
|
const float* pr = scene.proj;
|
||||||
|
if (pr[11] != 1.0f || pr[0] == 0.0f || pr[5] == 0.0f || pr[14] == 0.0f) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!EnsureVolumetricPipeline(context)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
nrhi::Device* device = context.device;
|
||||||
|
const uint32_t width = context.guest_output_width;
|
||||||
|
const uint32_t height = context.guest_output_height;
|
||||||
|
|
||||||
|
// ---- Full-res linear view-Z: the SSAO plane when AO linearized this
|
||||||
|
// frame (it idles back in RENDER_TARGET after the AO pass), else the own
|
||||||
|
// linearize below.
|
||||||
|
bool lin_ready = ssao_ran && g_r.ao_lin_depth != nullptr &&
|
||||||
|
g_r.ao_lin_srv != nullptr && g_r.ao_lin_width == width &&
|
||||||
|
g_r.ao_lin_height == height;
|
||||||
|
if (!lin_ready) {
|
||||||
|
if (g_r.vol_lin_depth == nullptr || g_r.vol_lin_width != width ||
|
||||||
|
g_r.vol_lin_height != height) {
|
||||||
|
if (g_r.vol_lin_srv != nullptr) {
|
||||||
|
device->DestroyDeferred(g_r.vol_lin_srv);
|
||||||
|
g_r.vol_lin_srv = nullptr;
|
||||||
|
}
|
||||||
|
if (g_r.vol_lin_depth != nullptr) {
|
||||||
|
device->DestroyDeferred(g_r.vol_lin_depth);
|
||||||
|
g_r.vol_lin_depth = nullptr;
|
||||||
|
}
|
||||||
|
nrhi::TextureDesc desc;
|
||||||
|
desc.width = width;
|
||||||
|
desc.height = height;
|
||||||
|
desc.mip_levels = 1;
|
||||||
|
desc.format = nrhi::Format::kR32_FLOAT;
|
||||||
|
desc.usage = nrhi::kTextureUsageRenderTarget;
|
||||||
|
desc.initial_state = nrhi::ResourceState::kRenderTarget;
|
||||||
|
g_r.vol_lin_depth = device->CreateTexture(desc);
|
||||||
|
if (g_r.vol_lin_depth == nullptr) {
|
||||||
|
g_r.vol_failed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
nrhi::TextureViewDesc vd;
|
||||||
|
vd.mip_levels = 1;
|
||||||
|
g_r.vol_lin_srv = device->CreateTextureView(g_r.vol_lin_depth, vd);
|
||||||
|
if (g_r.vol_lin_srv == nullptr) {
|
||||||
|
g_r.vol_failed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_r.vol_lin_width = width;
|
||||||
|
g_r.vol_lin_height = height;
|
||||||
|
}
|
||||||
|
// Scene-depth SRV, re-pointed when the depth buffer is rebuilt.
|
||||||
|
if (g_r.vol_depth_srv == nullptr || g_r.vol_depth_srv_of != g_r.depth) {
|
||||||
|
if (g_r.vol_depth_srv != nullptr) {
|
||||||
|
device->DestroyDeferred(g_r.vol_depth_srv);
|
||||||
|
g_r.vol_depth_srv = nullptr;
|
||||||
|
}
|
||||||
|
nrhi::TextureViewDesc sd_desc;
|
||||||
|
if (g_r.msaa > 1) {
|
||||||
|
sd_desc.dimension = nrhi::ViewDimension::k2DMS;
|
||||||
|
} else {
|
||||||
|
sd_desc.dimension = nrhi::ViewDimension::k2D;
|
||||||
|
sd_desc.mip_levels = 1;
|
||||||
|
}
|
||||||
|
g_r.vol_depth_srv = device->CreateTextureView(g_r.depth, sd_desc);
|
||||||
|
if (g_r.vol_depth_srv == nullptr) {
|
||||||
|
g_r.vol_failed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_r.vol_depth_srv_of = g_r.depth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Shaft planes (half res, march + blurred), rebuilt on resize.
|
||||||
|
const uint32_t vw = std::max(1u, (width + 1) / 2);
|
||||||
|
const uint32_t vh = std::max(1u, (height + 1) / 2);
|
||||||
|
if (shafts &&
|
||||||
|
(g_r.vol_width != vw || g_r.vol_height != vh || g_r.vol_tex == nullptr ||
|
||||||
|
g_r.vol_tex_b == nullptr)) {
|
||||||
|
nrhi::Texture** texs[2] = {&g_r.vol_tex, &g_r.vol_tex_b};
|
||||||
|
nrhi::TextureView** views[2] = {&g_r.vol_srv, &g_r.vol_srv_b};
|
||||||
|
for (int i = 0; i < 2; ++i) {
|
||||||
|
if (*views[i] != nullptr) {
|
||||||
|
device->DestroyDeferred(*views[i]);
|
||||||
|
*views[i] = nullptr;
|
||||||
|
}
|
||||||
|
if (*texs[i] != nullptr) {
|
||||||
|
device->DestroyDeferred(*texs[i]);
|
||||||
|
*texs[i] = nullptr;
|
||||||
|
}
|
||||||
|
nrhi::TextureDesc desc;
|
||||||
|
desc.width = vw;
|
||||||
|
desc.height = vh;
|
||||||
|
desc.mip_levels = 1;
|
||||||
|
desc.format = nrhi::Format::kR16G16B16A16_FLOAT;
|
||||||
|
desc.usage = nrhi::kTextureUsageRenderTarget;
|
||||||
|
desc.initial_state = nrhi::ResourceState::kRenderTarget;
|
||||||
|
*texs[i] = device->CreateTexture(desc);
|
||||||
|
if (*texs[i] == nullptr) {
|
||||||
|
g_r.vol_failed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
nrhi::TextureViewDesc vd;
|
||||||
|
vd.mip_levels = 1;
|
||||||
|
*views[i] = device->CreateTextureView(*texs[i], vd);
|
||||||
|
if (*views[i] == nullptr) {
|
||||||
|
g_r.vol_failed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_r.vol_width = vw;
|
||||||
|
g_r.vol_height = vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sun direction in the AO view space (the projection's per-axis signs
|
||||||
|
// folded into the world->view rotation, exactly as the SSR pass builds
|
||||||
|
// it): the space ps_tonemap reconstructs view rays in for the haze.
|
||||||
|
const float* sd = scene.sky_sun; // [0..2] = unit vector toward the sun
|
||||||
|
const float* vpm = scene.view_proj;
|
||||||
|
const float sxs = pr[0] < 0.0f ? -1.0f : 1.0f;
|
||||||
|
const float sys = pr[5] < 0.0f ? -1.0f : 1.0f;
|
||||||
|
const float inv00 = 1.0f / pr[0];
|
||||||
|
const float inv11 = 1.0f / pr[5];
|
||||||
|
float sun_view[3] = {
|
||||||
|
sd[0] * vpm[0] * inv00 * sxs + sd[1] * vpm[4] * inv00 * sxs +
|
||||||
|
sd[2] * vpm[8] * inv00 * sxs,
|
||||||
|
sd[0] * vpm[1] * inv11 * sys + sd[1] * vpm[5] * inv11 * sys +
|
||||||
|
sd[2] * vpm[9] * inv11 * sys,
|
||||||
|
sd[0] * vpm[3] + sd[1] * vpm[7] + sd[2] * vpm[11]};
|
||||||
|
const float svl = std::sqrt(sun_view[0] * sun_view[0] +
|
||||||
|
sun_view[1] * sun_view[1] +
|
||||||
|
sun_view[2] * sun_view[2]);
|
||||||
|
if (svl > 1e-6f) {
|
||||||
|
sun_view[0] /= svl;
|
||||||
|
sun_view[1] /= svl;
|
||||||
|
sun_view[2] /= svl;
|
||||||
|
}
|
||||||
|
// Haze tint: the frame's captured linear fog color folded through the
|
||||||
|
// scene exposure: the fully-fogged xe value, so the haze always matches
|
||||||
|
// the game's own atmosphere (and fades out at night with it).
|
||||||
|
const float expo = scene.sky_sun[5] > 0.0f ? scene.sky_sun[5] : 2.5f;
|
||||||
|
const float haze_int =
|
||||||
|
haze_want ? float(REXCVAR_GET(skate3_native_render_scene_haze_intensity))
|
||||||
|
: 0.0f;
|
||||||
|
const float shaft_int =
|
||||||
|
shafts ? float(REXCVAR_GET(skate3_native_render_scene_shafts_intensity))
|
||||||
|
: 0.0f;
|
||||||
|
|
||||||
|
// The march reconstructs world positions through the inverse
|
||||||
|
// view-projection.
|
||||||
|
float inv_vp[16];
|
||||||
|
const bool inv_ok = Invert4x4(scene.view_proj, inv_vp);
|
||||||
|
|
||||||
|
const uint32_t cb_offset =
|
||||||
|
uint32_t(frame_number % RendererState::kShadowCbRegions) *
|
||||||
|
RendererState::kShadowCbSlice;
|
||||||
|
cmd->SetBindingLayout(g_r.hdr_layout);
|
||||||
|
cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList);
|
||||||
|
cmd->SetConstantBuffer(6, g_r.shadow_cb, cb_offset);
|
||||||
|
|
||||||
|
float c[32] = {};
|
||||||
|
// 1) Own linearize when AO did not produce the plane this frame (needs
|
||||||
|
// the m22/m32 rows in the vp slot).
|
||||||
|
nrhi::Texture* lin_tex = lin_ready ? g_r.ao_lin_depth : g_r.vol_lin_depth;
|
||||||
|
nrhi::TextureView* lin_srv = lin_ready ? g_r.ao_lin_srv : g_r.vol_lin_srv;
|
||||||
|
if (!lin_ready) {
|
||||||
|
c[16] = std::fabs(pr[0]);
|
||||||
|
c[17] = std::fabs(pr[5]);
|
||||||
|
c[18] = pr[10];
|
||||||
|
c[19] = pr[14];
|
||||||
|
cmd->SetRootConstants(0, 32, c, 0);
|
||||||
|
cmd->Barrier(g_r.depth, nrhi::ResourceState::kDepthWrite,
|
||||||
|
nrhi::ResourceState::kPixelShaderResource);
|
||||||
|
cmd->FlushBarriers();
|
||||||
|
cmd->SetViewport(viewport);
|
||||||
|
cmd->SetScissor(scissor);
|
||||||
|
cmd->SetRenderTargets(g_r.vol_lin_depth, nullptr);
|
||||||
|
cmd->SetPipeline(g_r.pso_vol_linearize);
|
||||||
|
cmd->SetTexture(1, g_r.vol_depth_srv);
|
||||||
|
cmd->Draw(3, 0);
|
||||||
|
cmd->Barrier(g_r.depth, nrhi::ResourceState::kPixelShaderResource,
|
||||||
|
nrhi::ResourceState::kDepthWrite);
|
||||||
|
}
|
||||||
|
cmd->Barrier(lin_tex, nrhi::ResourceState::kRenderTarget,
|
||||||
|
nrhi::ResourceState::kPixelShaderResource);
|
||||||
|
cmd->FlushBarriers();
|
||||||
|
|
||||||
|
// 2) Shadow-marched shafts (half res): world-space camera->pixel march
|
||||||
|
// testing per-step sun visibility against the CSM atlas + the static
|
||||||
|
// world-shadow map (see ps_vol_shafts).
|
||||||
|
bool shaft_plane = false;
|
||||||
|
if (shafts && inv_ok && g_r.vol_tex != nullptr && g_r.vol_tex_b != nullptr) {
|
||||||
|
const nrhi::Viewport vol_vp{0.0f, 0.0f, float(vw), float(vh), 0.0f, 1.0f};
|
||||||
|
const nrhi::Rect vol_sc{0, 0, int32_t(vw), int32_t(vh)};
|
||||||
|
c[0] = float(vw);
|
||||||
|
c[1] = float(vh);
|
||||||
|
c[2] = 1.0f / float(vw);
|
||||||
|
c[3] = 1.0f / float(vh);
|
||||||
|
c[4] = scene.cam_pos[0];
|
||||||
|
c[5] = scene.cam_pos[1];
|
||||||
|
c[6] = scene.cam_pos[2];
|
||||||
|
c[7] = float(REXCVAR_GET(skate3_native_render_scene_shafts_reach));
|
||||||
|
c[8] = float(REXCVAR_GET(skate3_native_render_scene_shafts_steps));
|
||||||
|
c[10] = 0.6f; // Henyey-Greenstein forward-scatter anisotropy
|
||||||
|
c[12] = pr[10];
|
||||||
|
c[13] = pr[14];
|
||||||
|
std::memcpy(c + 16, inv_vp, sizeof(inv_vp));
|
||||||
|
cmd->SetRootConstants(0, 32, c, 0);
|
||||||
|
cmd->SetViewport(vol_vp);
|
||||||
|
cmd->SetScissor(vol_sc);
|
||||||
|
cmd->SetRenderTargets(g_r.vol_tex, nullptr);
|
||||||
|
cmd->SetPipeline(g_r.pso_vol_shafts);
|
||||||
|
cmd->SetTexture(2, lin_srv); // t1 = linear depth
|
||||||
|
cmd->SetTexture(3, g_r.shadow_srv_final); // t2 = CSM atlas
|
||||||
|
// t3 = the static world-shadow map when primed (white is neutral: the
|
||||||
|
// stored depth 1 compares lit everywhere).
|
||||||
|
cmd->SetTexture(4, (g_r.world_shadow_srv != nullptr &&
|
||||||
|
g_r.world_shadow_in_srv)
|
||||||
|
? g_r.world_shadow_srv
|
||||||
|
: g_r.white.srv);
|
||||||
|
cmd->Draw(3, 0);
|
||||||
|
cmd->Barrier(g_r.vol_tex, nrhi::ResourceState::kRenderTarget,
|
||||||
|
nrhi::ResourceState::kPixelShaderResource);
|
||||||
|
cmd->FlushBarriers();
|
||||||
|
// 3x3 tent blur (integrates the march jitter, softens shadow-volume
|
||||||
|
// edges); the blurred plane is what ps_tonemap samples.
|
||||||
|
c[4] = float(vw);
|
||||||
|
c[5] = float(vh);
|
||||||
|
c[6] = 1.0f / float(vw);
|
||||||
|
c[7] = 1.0f / float(vh);
|
||||||
|
cmd->SetRootConstants(0, 8, c, 0);
|
||||||
|
cmd->SetRenderTargets(g_r.vol_tex_b, nullptr);
|
||||||
|
cmd->SetPipeline(g_r.pso_vol_blur);
|
||||||
|
cmd->SetTexture(1, g_r.vol_srv);
|
||||||
|
cmd->Draw(3, 0);
|
||||||
|
cmd->Barrier(g_r.vol_tex, nrhi::ResourceState::kPixelShaderResource,
|
||||||
|
nrhi::ResourceState::kRenderTarget);
|
||||||
|
cmd->Barrier(g_r.vol_tex_b, nrhi::ResourceState::kRenderTarget,
|
||||||
|
nrhi::ResourceState::kPixelShaderResource);
|
||||||
|
cmd->FlushBarriers();
|
||||||
|
shaft_plane = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Stage ps_tonemap's volumetric rows + the plane handoffs.
|
||||||
|
g_r.vol_rows[0] = std::fabs(pr[0]);
|
||||||
|
g_r.vol_rows[1] = std::fabs(pr[5]);
|
||||||
|
g_r.vol_rows[2] = pr[10];
|
||||||
|
g_r.vol_rows[3] = pr[14];
|
||||||
|
g_r.vol_rows[4] = 0.0f;
|
||||||
|
g_r.vol_rows[5] = 0.0f;
|
||||||
|
g_r.vol_rows[6] = 0.0f;
|
||||||
|
g_r.vol_rows[7] = shaft_plane ? shaft_int : 0.0f;
|
||||||
|
g_r.vol_rows[8] = scene.fog_color[0] * expo;
|
||||||
|
g_r.vol_rows[9] = scene.fog_color[1] * expo;
|
||||||
|
g_r.vol_rows[10] = scene.fog_color[2] * expo;
|
||||||
|
g_r.vol_rows[11] = haze_int;
|
||||||
|
g_r.vol_rows[12] = sun_view[0];
|
||||||
|
g_r.vol_rows[13] = sun_view[1];
|
||||||
|
g_r.vol_rows[14] = sun_view[2];
|
||||||
|
g_r.vol_rows[15] =
|
||||||
|
float(REXCVAR_GET(skate3_native_render_scene_haze_density));
|
||||||
|
g_r.vol_tonemap_valid = true;
|
||||||
|
g_r.vol_plane_in_psr = shaft_plane;
|
||||||
|
g_r.vol_lin_plane = lin_tex;
|
||||||
|
g_r.vol_lin_plane_srv = lin_srv;
|
||||||
|
g_r.vol_lin_in_psr = true;
|
||||||
|
// Throttled diagnostics: the terms degrade to zero silently when the
|
||||||
|
// shadow state or the fog capture is missing; log what decides them.
|
||||||
|
static uint32_t s_vol_log = 0;
|
||||||
|
if (s_vol_log < 4 || (s_vol_log % 36000) == 0) {
|
||||||
|
REXLOG_INFO(
|
||||||
|
"native-scene: vol2 shafts={} gates[cvar={} shvalid={} atlas={} "
|
||||||
|
"insrv={}] inv={} ws={} tex={} tint=({:.3f},{:.3f},{:.3f}) "
|
||||||
|
"expo={:.2f} sunv=({:.2f},{:.2f},{:.2f}) lin={}",
|
||||||
|
shaft_plane ? 1 : 0,
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_shafts) ? 1 : 0,
|
||||||
|
scene.shadow_valid ? 1 : 0, g_r.shadow_srv_final != nullptr ? 1 : 0,
|
||||||
|
g_r.shadow_in_srv_state ? 1 : 0, inv_ok ? 1 : 0,
|
||||||
|
g_r.world_shadow_in_srv ? 1 : 0, g_r.vol_tex != nullptr ? 1 : 0,
|
||||||
|
g_r.vol_rows[8], g_r.vol_rows[9], g_r.vol_rows[10], expo, sun_view[0],
|
||||||
|
sun_view[1], sun_view[2], lin_ready ? "ao" : "own");
|
||||||
|
}
|
||||||
|
++s_vol_log;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- HDR post chain (hdr.hlsl: bloom pyramid + the extracted tonemap) ----
|
// ---- HDR post chain (hdr.hlsl: bloom pyramid + the extracted tonemap) ----
|
||||||
// Layout + PSOs, built when the HDR path activates and rebuilt when the
|
// Layout + PSOs, built when the HDR path activates and rebuilt when the
|
||||||
// guest output format (the tonemap target) changes. Failure falls back to
|
// guest output format (the tonemap target) changes. Failure falls back to
|
||||||
|
|
@ -867,12 +1306,17 @@ bool EnsureHdrPipeline(const NativeGuestOutputRenderContext& context) {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
if (g_r.hdr_layout == nullptr) {
|
if (g_r.hdr_layout == nullptr) {
|
||||||
// The SSAO layout shape plus a third single-texture table (t2 = the
|
// The SSAO layout shape widened for the fused tonemap consumers: root
|
||||||
// fused AO multiplier plane): root constants b0 + three single-texture
|
// constants b0 (32 floats: the tail rows carry the volumetric params)
|
||||||
// tables + point/linear clamp samplers (hdr.hlsl's frozen Vulkan plan).
|
// + five single-texture tables (t2 = the fused AO multiplier plane,
|
||||||
|
// t3 = the shaft plane, t4 = linear depth) + point/linear clamp
|
||||||
|
// samplers + the per-frame shadow constant slice at b1 (the volumetric
|
||||||
|
// march samples sun visibility with the material receiver math). The
|
||||||
|
// CBV is APPENDED so the table param indices stay 1..5 (Vulkan's set-0
|
||||||
|
// derivation orders buffer params by declaration order regardless).
|
||||||
nrhi::BindingLayoutDesc ld;
|
nrhi::BindingLayoutDesc ld;
|
||||||
ld.param_count = 4;
|
ld.param_count = 7;
|
||||||
ld.params[0] = {nrhi::BindingParamKind::kConstants, /*b*/ 0, 16,
|
ld.params[0] = {nrhi::BindingParamKind::kConstants, /*b*/ 0, 32,
|
||||||
nrhi::Visibility::kAll};
|
nrhi::Visibility::kAll};
|
||||||
ld.params[1] = {nrhi::BindingParamKind::kTextureTable, /*t*/ 0, 1,
|
ld.params[1] = {nrhi::BindingParamKind::kTextureTable, /*t*/ 0, 1,
|
||||||
nrhi::Visibility::kPixel};
|
nrhi::Visibility::kPixel};
|
||||||
|
|
@ -880,6 +1324,12 @@ bool EnsureHdrPipeline(const NativeGuestOutputRenderContext& context) {
|
||||||
nrhi::Visibility::kPixel};
|
nrhi::Visibility::kPixel};
|
||||||
ld.params[3] = {nrhi::BindingParamKind::kTextureTable, 2, 1,
|
ld.params[3] = {nrhi::BindingParamKind::kTextureTable, 2, 1,
|
||||||
nrhi::Visibility::kPixel};
|
nrhi::Visibility::kPixel};
|
||||||
|
ld.params[4] = {nrhi::BindingParamKind::kTextureTable, 3, 1,
|
||||||
|
nrhi::Visibility::kPixel};
|
||||||
|
ld.params[5] = {nrhi::BindingParamKind::kTextureTable, 4, 1,
|
||||||
|
nrhi::Visibility::kPixel};
|
||||||
|
ld.params[6] = {nrhi::BindingParamKind::kConstantBuffer, /*b*/ 1, 1,
|
||||||
|
nrhi::Visibility::kPixel};
|
||||||
ld.static_sampler_count = 2;
|
ld.static_sampler_count = 2;
|
||||||
ld.static_samplers[0] = {/*s*/ 0, nrhi::Filter::kPoint,
|
ld.static_samplers[0] = {/*s*/ 0, nrhi::Filter::kPoint,
|
||||||
nrhi::AddressMode::kClamp, 1};
|
nrhi::AddressMode::kClamp, 1};
|
||||||
|
|
@ -966,7 +1416,8 @@ bool EnsureHdrPipeline(const NativeGuestOutputRenderContext& context) {
|
||||||
// consumer expects); the caller restores the main binding layout after.
|
// consumer expects); the caller restores the main binding layout after.
|
||||||
void ApplyHdrPost(const NativeGuestOutputRenderContext& context,
|
void ApplyHdrPost(const NativeGuestOutputRenderContext& context,
|
||||||
nrhi::Cmd* cmd, const nrhi::Viewport& viewport,
|
nrhi::Cmd* cmd, const nrhi::Viewport& viewport,
|
||||||
const nrhi::Rect& scissor, bool loading_native) {
|
const nrhi::Rect& scissor, bool loading_native,
|
||||||
|
uint64_t frame_number) {
|
||||||
nrhi::Device* device = context.device;
|
nrhi::Device* device = context.device;
|
||||||
const uint32_t width = context.guest_output_width;
|
const uint32_t width = context.guest_output_width;
|
||||||
const uint32_t height = context.guest_output_height;
|
const uint32_t height = context.guest_output_height;
|
||||||
|
|
@ -1034,12 +1485,26 @@ void ApplyHdrPost(const NativeGuestOutputRenderContext& context,
|
||||||
|
|
||||||
cmd->SetBindingLayout(g_r.hdr_layout);
|
cmd->SetBindingLayout(g_r.hdr_layout);
|
||||||
cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList);
|
cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList);
|
||||||
|
// The layout carries the shadow constant slice at b1 (the volumetric
|
||||||
|
// march); bind it for every draw on this layout; the bloom/tonemap
|
||||||
|
// shaders never read it, but the descriptor must hold a valid buffer.
|
||||||
|
if (g_r.shadow_cb != nullptr) {
|
||||||
|
const uint32_t cb_offset =
|
||||||
|
uint32_t(frame_number % RendererState::kShadowCbRegions) *
|
||||||
|
RendererState::kShadowCbSlice;
|
||||||
|
cmd->SetConstantBuffer(6, g_r.shadow_cb, cb_offset);
|
||||||
|
}
|
||||||
// The scene plane is the sampled source for the whole chain.
|
// The scene plane is the sampled source for the whole chain.
|
||||||
cmd->Barrier(g_r.hdr_resolved, nrhi::ResourceState::kRenderTarget,
|
cmd->Barrier(g_r.hdr_resolved, nrhi::ResourceState::kRenderTarget,
|
||||||
nrhi::ResourceState::kPixelShaderResource);
|
nrhi::ResourceState::kPixelShaderResource);
|
||||||
cmd->FlushBarriers();
|
cmd->FlushBarriers();
|
||||||
|
|
||||||
float c[16];
|
float c[32] = {};
|
||||||
|
// Rows 16-31 = the volumetric params ApplyVolumetricPass staged this
|
||||||
|
// frame (zeros otherwise; every volumetric term drops out).
|
||||||
|
if (g_r.vol_tonemap_valid) {
|
||||||
|
std::memcpy(c + 16, g_r.vol_rows, sizeof(g_r.vol_rows));
|
||||||
|
}
|
||||||
const auto set_consts = [&](uint32_t dw, uint32_t dh, uint32_t sw,
|
const auto set_consts = [&](uint32_t dw, uint32_t dh, uint32_t sw,
|
||||||
uint32_t sh, float intensity, float tap_scale) {
|
uint32_t sh, float intensity, float tap_scale) {
|
||||||
c[0] = float(dw);
|
c[0] = float(dw);
|
||||||
|
|
@ -1062,7 +1527,7 @@ void ApplyHdrPost(const NativeGuestOutputRenderContext& context,
|
||||||
}
|
}
|
||||||
c[12] = tap_scale;
|
c[12] = tap_scale;
|
||||||
c[13] = c[14] = c[15] = 0.0f;
|
c[13] = c[14] = c[15] = 0.0f;
|
||||||
cmd->SetRootConstants(0, 16, c, 0);
|
cmd->SetRootConstants(0, 32, c, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (bloom) {
|
if (bloom) {
|
||||||
|
|
@ -1128,6 +1593,11 @@ void ApplyHdrPost(const NativeGuestOutputRenderContext& context,
|
||||||
cmd->SetTexture(2, bloom ? g_r.bloom_srv[0] : g_r.white.srv);
|
cmd->SetTexture(2, bloom ? g_r.bloom_srv[0] : g_r.white.srv);
|
||||||
// t2 = the fused AO multiplier plane (white = no AO this frame).
|
// t2 = the fused AO multiplier plane (white = no AO this frame).
|
||||||
cmd->SetTexture(3, g_r.ao_plane_in_psr ? g_r.ao_srv[0] : g_r.white.srv);
|
cmd->SetTexture(3, g_r.ao_plane_in_psr ? g_r.ao_srv[0] : g_r.white.srv);
|
||||||
|
// t3/t4 = the volumetric shaft plane + linear depth (white with
|
||||||
|
// zero-weight constant rows when the pass did not run).
|
||||||
|
cmd->SetTexture(4, g_r.vol_plane_in_psr ? g_r.vol_srv_b : g_r.white.srv);
|
||||||
|
cmd->SetTexture(5, g_r.vol_lin_in_psr ? g_r.vol_lin_plane_srv
|
||||||
|
: g_r.white.srv);
|
||||||
cmd->Draw(3, 0);
|
cmd->Draw(3, 0);
|
||||||
|
|
||||||
// Restore steady states.
|
// Restore steady states.
|
||||||
|
|
@ -1138,6 +1608,19 @@ void ApplyHdrPost(const NativeGuestOutputRenderContext& context,
|
||||||
nrhi::ResourceState::kRenderTarget);
|
nrhi::ResourceState::kRenderTarget);
|
||||||
g_r.ao_plane_in_psr = false;
|
g_r.ao_plane_in_psr = false;
|
||||||
}
|
}
|
||||||
|
if (g_r.vol_plane_in_psr) {
|
||||||
|
cmd->Barrier(g_r.vol_tex_b, nrhi::ResourceState::kPixelShaderResource,
|
||||||
|
nrhi::ResourceState::kRenderTarget);
|
||||||
|
g_r.vol_plane_in_psr = false;
|
||||||
|
}
|
||||||
|
if (g_r.vol_lin_in_psr) {
|
||||||
|
cmd->Barrier(g_r.vol_lin_plane, nrhi::ResourceState::kPixelShaderResource,
|
||||||
|
nrhi::ResourceState::kRenderTarget);
|
||||||
|
g_r.vol_lin_in_psr = false;
|
||||||
|
g_r.vol_lin_plane = nullptr;
|
||||||
|
g_r.vol_lin_plane_srv = nullptr;
|
||||||
|
}
|
||||||
|
g_r.vol_tonemap_valid = false;
|
||||||
if (bloom) {
|
if (bloom) {
|
||||||
for (uint32_t i = 0; i < g_r.bloom_levels; ++i) {
|
for (uint32_t i = 0; i < g_r.bloom_levels; ++i) {
|
||||||
cmd->Barrier(g_r.bloom_tex[i],
|
cmd->Barrier(g_r.bloom_tex[i],
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,10 @@ inline bool g_sky_frame_done = false;
|
||||||
// exposure (c3.x). Persist like g_sky_height once captured.
|
// exposure (c3.x). Persist like g_sky_height once captured.
|
||||||
inline float g_sky_sun[6] = {};
|
inline float g_sky_sun[6] = {};
|
||||||
inline bool g_sky_sun_have = false;
|
inline bool g_sky_sun_have = false;
|
||||||
|
// The CAPTURED (un-overridden) sun direction, published each frame for the
|
||||||
|
// debug dialog: enabling the sun override seeds its azimuth/elevation from
|
||||||
|
// this so the sun starts at its true position instead of jumping.
|
||||||
|
inline std::atomic<float> g_sun_captured[3] = {0.0f, 1.0f, 0.0f};
|
||||||
// UI background blur (see FrameScene::ui_blur and kBlurShaderSource): while
|
// UI background blur (see FrameScene::ui_blur and kBlurShaderSource): while
|
||||||
// a frontend popup is up the game appends blur_hBlur/vBlur + basictex passes
|
// a frontend popup is up the game appends blur_hBlur/vBlur + basictex passes
|
||||||
// after the postfx uber. g_ui_blur holds the captured kernel scale (PS c0.x,
|
// after the postfx uber. g_ui_blur holds the captured kernel scale (PS c0.x,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue