render: cascaded static sun-shadow map with contact-hardening soft shadows
This commit is contained in:
parent
f4cd327b8a
commit
f11aa9ece4
6 changed files with 14049 additions and 5530 deletions
|
|
@ -94,6 +94,33 @@ cbuffer S : register(b1) {
|
||||||
// oceanreflection_defaultPS row (fam 32): x = multiplier, y/z = the
|
// oceanreflection_defaultPS row (fam 32): x = multiplier, y/z = the
|
||||||
// world-height alpha fade bounds.
|
// world-height alpha fade bounds.
|
||||||
float4 orf;
|
float4 orf;
|
||||||
|
// Contact-hardening soft shadows (PCSS over the same cascade atlas; see
|
||||||
|
// SampleCsmShadowSoft): x = enable, y = tan of the sun's angular radius
|
||||||
|
// (penumbra width per meter of caster distance), z = max penumbra
|
||||||
|
// half-width in meters, w = static-caster receive bias in depth units
|
||||||
|
// (slope-scaled in the sampler; zero when static geometry is not in the
|
||||||
|
// atlas).
|
||||||
|
float4 sh_pcss;
|
||||||
|
// x = blocker-search radius in meters, y = meters per depth-map unit
|
||||||
|
// (1 / |sh_z.xyz|), z = light-space units per meter (|sh_x.xyz|, base
|
||||||
|
// cascade), w = minimum filter radius in physical atlas texels.
|
||||||
|
float4 sh_pcss2;
|
||||||
|
// Native static sun-shadow map transform (see SampleStaticSun): the
|
||||||
|
// world -> FAR-tile rows. uc = dot(nsm_x.xyz, wp) + nsm_x.w in [-1,1]
|
||||||
|
// (same for y), depth = dot(nsm_z.xyz, wp) + nsm_z.w in [0,1]. The near
|
||||||
|
// tile shares the origin, so its coordinate is exactly uc * nsm_p2.z.
|
||||||
|
float4 nsm_x;
|
||||||
|
float4 nsm_y;
|
||||||
|
float4 nsm_z;
|
||||||
|
// x = static shadow strength (0 = feature off), y = far-tile uc units
|
||||||
|
// per meter (1 / far radius), z = meters per depth unit, w = minimum
|
||||||
|
// filter radius in tile uc units.
|
||||||
|
float4 nsm_p;
|
||||||
|
// x = base receiver bias (depth units), y = slope-scaled bias term
|
||||||
|
// (depth units), z = reserved (cascade ratios are the shader-side
|
||||||
|
// kRatioInner/kRatioMid constants), w = tile size in pixels (tap
|
||||||
|
// clamping).
|
||||||
|
float4 nsm_p2;
|
||||||
};
|
};
|
||||||
// Character-family lighting (defaultcharacter.fx and friends): canonical
|
// Character-family lighting (defaultcharacter.fx and friends): canonical
|
||||||
// per-draw rows captured from the guest PIXEL constant bank at palette
|
// per-draw rows captured from the guest PIXEL constant bank at palette
|
||||||
|
|
@ -126,6 +153,11 @@ TextureCube<float4> env_cube : register(t6);
|
||||||
// dynamicobject families; see those branches).
|
// dynamicobject families; see those branches).
|
||||||
Texture2D<float4> detail_map : register(t8);
|
Texture2D<float4> detail_map : register(t8);
|
||||||
Texture2D<float4> spec2_map : register(t9);
|
Texture2D<float4> spec2_map : register(t9);
|
||||||
|
// Native static sun-shadow map (third entry of the t8/t9 table): a single
|
||||||
|
// camera-centered ortho depth map of the STATIC world along the material
|
||||||
|
// sun, rendered natively each frame. Sampled by SampleStaticSun as an
|
||||||
|
// additional receive term on every lit branch.
|
||||||
|
Texture2D<float2> static_sun : register(t10);
|
||||||
// Raw bone palette: 3 float4 rows per bone, column-vector affine [R | t],
|
// Raw bone palette: 3 float4 rows per bone, column-vector affine [R | t],
|
||||||
// applied with explicit dots (StructuredBuffer<float4x4> default packing is
|
// applied with explicit dots (StructuredBuffer<float4x4> default packing is
|
||||||
// column-major and would silently transpose the matrices).
|
// column-major and would silently transpose the matrices).
|
||||||
|
|
@ -263,6 +295,276 @@ float SampleCsmShadow(float3 wp, float extra_bias) {
|
||||||
float2 sm2 = shadow_atlas.Sample(smp_clamp, suv);
|
float2 sm2 = shadow_atlas.Sample(smp_clamp, suv);
|
||||||
return saturate((sm2.x >= rd ? 1.0 : 0.0) + (1.0 - sm2.y));
|
return saturate((sm2.x >= rd ? 1.0 : 0.0) + (1.0 - sm2.y));
|
||||||
}
|
}
|
||||||
|
// Contact-hardening soft shadows (PCSS) over the same cascade atlas,
|
||||||
|
// enabled by sh_pcss.x. A blocker search estimates the average caster
|
||||||
|
// depth inside a world-space search radius; the penumbra half-width then
|
||||||
|
// follows the sun's angular size times the caster-to-receiver distance
|
||||||
|
// along the light axis (converted between world meters, depth-map units
|
||||||
|
// and light-space UV via the CPU-computed sh_pcss2 row scales), and a
|
||||||
|
// rotated poisson-disk PCF filters at that width; shadows are crisp at
|
||||||
|
// the contact point and soften with caster height, instead of the single
|
||||||
|
// fixed-width penumbra the blur chain bakes in. Tap semantics match the
|
||||||
|
// exact path (sat(infront + 1 - coverage); the blur pass dilates depth
|
||||||
|
// outward so filtered compares stay valid across coverage edges), and the
|
||||||
|
// per-class bias conventions of SampleCsmShadow carry over via extra_bias.
|
||||||
|
// The receiver normal drives an additional slope-scaled bias (sh_pcss.w)
|
||||||
|
// so large static casters, ground sheets, facades at grazing sun angles,
|
||||||
|
// never compare against their own atlas depth.
|
||||||
|
static const float2 kPcssTaps[12] = {
|
||||||
|
float2(-0.326212, -0.405805), float2(-0.840144, -0.073580),
|
||||||
|
float2(-0.695914, 0.457137), float2(-0.203345, 0.620716),
|
||||||
|
float2(0.962340, -0.194983), float2(0.473434, -0.480026),
|
||||||
|
float2(0.519456, 0.767022), float2(0.185461, -0.893124),
|
||||||
|
float2(0.507431, 0.064425), float2(0.896420, 0.412458),
|
||||||
|
float2(-0.321940, -0.932615), float2(-0.791559, -0.597705)};
|
||||||
|
float CsmSoftTap(float2 suv, float refd, float tile_x0, float tile_x1) {
|
||||||
|
// Taps clamp half a texel inside the tile so filtering never bleeds the
|
||||||
|
// neighboring cascade (same convention as the exact character taps).
|
||||||
|
suv.x = clamp(suv.x, tile_x0 + 0.5 / sh_misc.z, tile_x1 - 0.5 / sh_misc.z);
|
||||||
|
suv.y = clamp(suv.y, 0.5 / sh_misc.w, 1.0 - 0.5 / sh_misc.w);
|
||||||
|
float2 sm = shadow_atlas.Sample(smp_clamp, suv);
|
||||||
|
return saturate((sm.x >= refd ? 1.0 : 0.0) + (1.0 - sm.y));
|
||||||
|
}
|
||||||
|
// Poisson-rotation angle for the CSM atlas sampler: interleaved gradient
|
||||||
|
// noise of the SCREEN pixel: stable per pixel, smooth under motion. A
|
||||||
|
// world-position hash was rejected (sin(43758 * x) amplifies
|
||||||
|
// sub-millimeter float wobble of the reconstructed position into a fresh
|
||||||
|
// rotation every frame; penumbras boiled). The static sun-shadow sampler
|
||||||
|
// uses NO noise at all; its soft-compare Vogel kernel is smooth without
|
||||||
|
// it (verified offline), and every noise scheme read as speckle, stripes
|
||||||
|
// or grain there.
|
||||||
|
float PcssRotation(float2 px) {
|
||||||
|
return 6.2831853 *
|
||||||
|
frac(52.9829189 * frac(0.06711056 * px.x + 0.00583715 * px.y));
|
||||||
|
}
|
||||||
|
float SampleCsmShadowSoft(float3 wp, float extra_bias, float3 nrm,
|
||||||
|
float2 px) {
|
||||||
|
if (sh_pcss.x <= 0.5) {
|
||||||
|
return SampleCsmShadow(wp, extra_bias);
|
||||||
|
}
|
||||||
|
if (sh_misc.y <= 0.0) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
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 cscale = float2(1.0, 1.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; cscale = sh_c2.xy; }
|
||||||
|
float2 l1 = lsv * sh_c1.xy + sh_c1.zw;
|
||||||
|
if (max(abs(l1.x), abs(l1.y)) < 0.99) { luv = l1; casc = 2.0; cscale = sh_c1.xy; }
|
||||||
|
if (max(abs(lsv.x), abs(lsv.y)) < 0.99) { luv = lsv; casc = 1.0; cscale = float2(1.0, 1.0); }
|
||||||
|
if (casc <= 0.0) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
// Receiver bias: the same per-class conventions as SampleCsmShadow
|
||||||
|
// (character per-cascade rows / dynobj literals / world bias), plus the
|
||||||
|
// slope-scaled static term.
|
||||||
|
float bias;
|
||||||
|
if (extra_bias > 0.0 && sh_char.w > 0.0) {
|
||||||
|
bias = casc > 2.5 ? sh_char.z : (casc > 1.5 ? sh_char.y : sh_char.x);
|
||||||
|
} else if (extra_bias < 0.0) {
|
||||||
|
bias = casc > 1.5 ? 0.015 : 0.007;
|
||||||
|
} else {
|
||||||
|
bias = sh_misc.x + extra_bias;
|
||||||
|
}
|
||||||
|
float ndl = 1.0;
|
||||||
|
if (dot(nrm, nrm) > 1e-4) {
|
||||||
|
ndl = abs(dot(normalize(nrm), sh_sun.xyz));
|
||||||
|
}
|
||||||
|
float slope = sqrt(saturate(1.0 - ndl * ndl)) / max(ndl, 0.12);
|
||||||
|
bias += sh_pcss.w * (1.0 + slope);
|
||||||
|
float refd = dot(sh_z.xyz, wp) + sh_z.w - bias;
|
||||||
|
float2 suv0 = float2(luv.x / 6.0 + (casc * 2.0 - 1.0) / 6.0,
|
||||||
|
luv.y * -0.5 + 0.5);
|
||||||
|
float tile_x0 = (casc * 2.0 - 2.0) / 6.0;
|
||||||
|
float tile_x1 = casc * 2.0 / 6.0;
|
||||||
|
// Light-space UV per meter for this cascade; luv -> atlas uv is
|
||||||
|
// (x / 6, y / 2) (three tiles across, [-1,1] vertical).
|
||||||
|
float2 uvpm = sh_pcss2.z * cscale;
|
||||||
|
float2 luv_to_suv = float2(1.0 / 6.0, 0.5);
|
||||||
|
float ang = PcssRotation(px);
|
||||||
|
float2 rot = float2(cos(ang), sin(ang));
|
||||||
|
// Blocker search over 7 taps in the search radius. The penumbra follows
|
||||||
|
// the NEAREST blocker (max depth below the reference), not the average:
|
||||||
|
// the search disk can straddle unrelated far casters; a receiver near a
|
||||||
|
// building's shadow region would average the building's depth into its
|
||||||
|
// own contact shadow and smear it to the max radius (the fully-blurred
|
||||||
|
// character shadow). The nearest blocker is the one whose penumbra the
|
||||||
|
// receiver is actually inside.
|
||||||
|
float2 rsearch = sh_pcss2.x * uvpm * luv_to_suv;
|
||||||
|
float bmax = -1.0;
|
||||||
|
{
|
||||||
|
float2 s = suv0;
|
||||||
|
s.x = clamp(s.x, tile_x0 + 0.5 / sh_misc.z, tile_x1 - 0.5 / sh_misc.z);
|
||||||
|
s.y = clamp(s.y, 0.5 / sh_misc.w, 1.0 - 0.5 / sh_misc.w);
|
||||||
|
float d = shadow_atlas.Sample(smp_clamp, s).x;
|
||||||
|
if (d < refd) { bmax = max(bmax, d); }
|
||||||
|
}
|
||||||
|
[unroll] for (int k = 0; k < 12; k += 2) {
|
||||||
|
float2 o = kPcssTaps[k];
|
||||||
|
o = float2(o.x * rot.x - o.y * rot.y, o.x * rot.y + o.y * rot.x);
|
||||||
|
float2 s = suv0 + o * rsearch;
|
||||||
|
s.x = clamp(s.x, tile_x0 + 0.5 / sh_misc.z, tile_x1 - 0.5 / sh_misc.z);
|
||||||
|
s.y = clamp(s.y, 0.5 / sh_misc.w, 1.0 - 0.5 / sh_misc.w);
|
||||||
|
float d = shadow_atlas.Sample(smp_clamp, s).x;
|
||||||
|
if (d < refd) { bmax = max(bmax, d); }
|
||||||
|
}
|
||||||
|
if (bmax < 0.0) {
|
||||||
|
// No caster inside the search footprint: every filter tap would
|
||||||
|
// compare lit (uncovered texels hold the far clear).
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
float dist_m = max(refd - bmax, 0.0) * sh_pcss2.y;
|
||||||
|
float pen_m = min(dist_m * sh_pcss.y, sh_pcss.z);
|
||||||
|
// Filter radius: the penumbra in atlas units, floored at a fraction of a
|
||||||
|
// physical atlas texel so edges always stay filtered.
|
||||||
|
float2 rfilt = max(pen_m * uvpm, sh_pcss2.w * 2.0 / sh_misc.w) * luv_to_suv;
|
||||||
|
float acc = CsmSoftTap(suv0, refd, tile_x0, tile_x1);
|
||||||
|
[unroll] for (int j = 0; j < 12; ++j) {
|
||||||
|
float2 o = kPcssTaps[j];
|
||||||
|
o = float2(o.x * rot.x - o.y * rot.y, o.x * rot.y + o.y * rot.x);
|
||||||
|
acc += CsmSoftTap(suv0 + o * rfilt, refd, tile_x0, tile_x1);
|
||||||
|
}
|
||||||
|
return acc / 13.0;
|
||||||
|
}
|
||||||
|
// Native static sun-shadow receive (t10). The game's cascade transforms
|
||||||
|
// use a stylized near-vertical projection (fine for its own dynamic
|
||||||
|
// casters inside a ~12 m height window, ~40 degrees away from the
|
||||||
|
// material sun); tall static geometry needs a true sun-aligned
|
||||||
|
// projection, so static shade comes from this separate camera-centered
|
||||||
|
// ortho map instead of the CSM atlas. The term bottoms out at
|
||||||
|
// 1 - strength (nsm_p.x), keeping live static shade believable where the
|
||||||
|
// baked lighting's own sun disagrees, and fades out at the map edge so
|
||||||
|
// coverage never pops. Contact-hardening PCSS matching the atlas sampler
|
||||||
|
// (nearest-blocker search + rotated poisson PCF) when PCSS is enabled;
|
||||||
|
// a single compare otherwise. Uncovered texels hold the far clear = lit.
|
||||||
|
// Clamped tap into one tile of the three-tile static sun atlas (inner,
|
||||||
|
// mid, far left to right); taps never bleed across the tile seams.
|
||||||
|
float NsmTap(float2 suv, float refd, float tile_u0) {
|
||||||
|
float hx = 0.1666667 / nsm_p2.w; // half a texel in atlas u (3 tiles wide)
|
||||||
|
float hy = 0.5 / nsm_p2.w;
|
||||||
|
suv.x = clamp(suv.x, tile_u0 + hx, tile_u0 + 0.3333333 - hx);
|
||||||
|
suv.y = clamp(suv.y, hy, 1.0 - hy);
|
||||||
|
// Explicit LOD (the map is single-mip): the PCF loop below is dynamic
|
||||||
|
// and implicit-gradient sampling is not allowed in divergent flow.
|
||||||
|
return static_sun.SampleLevel(smp_clamp, suv, 0.0).x;
|
||||||
|
}
|
||||||
|
float SampleStaticSun(float3 wp, float3 nrm, float2 px) {
|
||||||
|
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);
|
||||||
|
// Screen-pixel footprint in far-tile uc, BEFORE any divergent return
|
||||||
|
// (gradient op; the enclosing family branches are uniform per draw).
|
||||||
|
// Scales the filter floor with viewing distance so far shadows
|
||||||
|
// integrate over the map's texel grid instead of aliasing against it:
|
||||||
|
// the shadow-map analogue of texture mip filtering. Close-ups keep the
|
||||||
|
// 2-texel floor untouched.
|
||||||
|
float pxfoot = max(fwidth(uc_far.x), fwidth(uc_far.y));
|
||||||
|
float edge = max(abs(uc_far.x), abs(uc_far.y));
|
||||||
|
if (edge >= 0.99) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
// Fade at the outer (far-tile) boundary only; the near->far handoff
|
||||||
|
// needs none (the far tile covers the near region too).
|
||||||
|
float fade = saturate((0.97 - edge) / 0.07);
|
||||||
|
// Cascade select (3 tiles sharing the origin, so each finer coordinate
|
||||||
|
// is an exact rescale of the far one). Ratios 6/2/1: the inner cascade
|
||||||
|
// trades peak density for reach; foliage dapple at 16x density read as
|
||||||
|
// over-detailed and only engaged very close; 6x at a third of the far
|
||||||
|
// radius is the calibrated compromise. Integer 1:2:6 chain keeps every
|
||||||
|
// tile on the far-texel snap grid. Keep in sync with
|
||||||
|
// RenderStaticSunMap's kRatioInner/kRatioMid.
|
||||||
|
const float kRatioInner = 6.0;
|
||||||
|
const float kRatioMid = 2.0;
|
||||||
|
float2 uc0 = uc_far * kRatioInner;
|
||||||
|
float2 uc1 = uc_far * kRatioMid;
|
||||||
|
float tile;
|
||||||
|
float2 uc;
|
||||||
|
float scale;
|
||||||
|
if (max(abs(uc0.x), abs(uc0.y)) < 0.99) {
|
||||||
|
tile = 0.0;
|
||||||
|
uc = uc0;
|
||||||
|
scale = kRatioInner;
|
||||||
|
} else if (max(abs(uc1.x), abs(uc1.y)) < 0.99) {
|
||||||
|
tile = 1.0;
|
||||||
|
uc = uc1;
|
||||||
|
scale = kRatioMid;
|
||||||
|
} else {
|
||||||
|
tile = 2.0;
|
||||||
|
uc = uc_far;
|
||||||
|
scale = 1.0;
|
||||||
|
}
|
||||||
|
float tile_u0 = tile / 3.0;
|
||||||
|
float ucpm = nsm_p.y * scale; // uc units per meter in this tile
|
||||||
|
float2 suv = float2(uc.x / 6.0 + (tile * 2.0 + 1.0) / 6.0,
|
||||||
|
uc.y * -0.5 + 0.5);
|
||||||
|
const float2 uc_to_suv = float2(1.0 / 6.0, 0.5);
|
||||||
|
float ndl = 1.0;
|
||||||
|
if (dot(nrm, nrm) > 1e-4) {
|
||||||
|
ndl = abs(dot(normalize(nrm), sh_sun.xyz));
|
||||||
|
}
|
||||||
|
float slope = sqrt(saturate(1.0 - ndl * ndl)) / max(ndl, 0.12);
|
||||||
|
float refd = dot(nsm_z.xyz, wp) + nsm_z.w - (nsm_p2.x + nsm_p2.y * slope);
|
||||||
|
if (sh_pcss.x <= 0.5) {
|
||||||
|
float s0 = NsmTap(suv, refd, tile_u0) >= refd ? 1.0 : 0.0;
|
||||||
|
return 1.0 - fade * nsm_p.x * (1.0 - s0);
|
||||||
|
}
|
||||||
|
// FIXED kernels throughout this sampler: no per-pixel noise. Verified
|
||||||
|
// offline against synthetic roof/wall/streetlamp depth maps at the real
|
||||||
|
// map scales: every noise scheme (world hash, interleaved gradient
|
||||||
|
// noise, white hash, radius jitter) renders the penumbra as speckle,
|
||||||
|
// stripes or grain, while fixed taps with the soft compares below give
|
||||||
|
// a clean smooth gradient (the penumbra fraction varies smoothly across
|
||||||
|
// receivers, and bilinear map sampling fills between taps). Denser
|
||||||
|
// blocker searches were also tried offline and REJECTED; unstable
|
||||||
|
// nearest-blocker picks around small casters draw dark veins.
|
||||||
|
float2 rsearch = sh_pcss2.x * ucpm * uc_to_suv;
|
||||||
|
float bmax = -1.0;
|
||||||
|
{
|
||||||
|
float d = NsmTap(suv, refd, tile_u0);
|
||||||
|
if (d < refd) { bmax = d; }
|
||||||
|
}
|
||||||
|
[unroll] for (int k = 0; k < 12; k += 3) {
|
||||||
|
float d = NsmTap(suv + kPcssTaps[k] * rsearch, refd, tile_u0);
|
||||||
|
if (d < refd) { bmax = max(bmax, d); }
|
||||||
|
}
|
||||||
|
if (bmax < 0.0) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
float dist_m = max(refd - bmax, 0.0) * nsm_p.z;
|
||||||
|
float pen_m = min(dist_m * sh_pcss.y, sh_pcss.z);
|
||||||
|
// Filter floor: 2 physical texels, raised to ~2 screen-pixel footprints
|
||||||
|
// at distance (lab-calibrated distance anti-aliasing).
|
||||||
|
float floor_uc = max(nsm_p.w, 2.0 * pxfoot * scale);
|
||||||
|
float r_uc = max(pen_m * ucpm, floor_uc);
|
||||||
|
float2 rfilt = r_uc * uc_to_suv;
|
||||||
|
// Vogel-spiral PCF with SOFT depth compares: binary taps quantize the
|
||||||
|
// penumbra into N discrete levels and the stable per-pixel rotation
|
||||||
|
// arranges the quantization into a visible cross-hatch ("fine mesh")
|
||||||
|
// pattern; a continuous per-tap contribution over a small depth window
|
||||||
|
// turns the same taps into a smooth gradient. The window rides the
|
||||||
|
// receiver bias so contact shadows stay grounded. Wide penumbras (tall
|
||||||
|
// roof edges cap the radius) undersample the base kernel and re-weave
|
||||||
|
// the mesh: they spend a second Vogel ring instead; the branch is
|
||||||
|
// spatially coherent across a penumbra.
|
||||||
|
float w = max(nsm_p2.x * 2.0, 1e-5);
|
||||||
|
// Tap count follows the FINAL radius (penumbra or adaptive floor,
|
||||||
|
// whichever won); wide kernels undersample at 13 taps.
|
||||||
|
int n = r_uc > 0.3 * ucpm ? 26 : 13;
|
||||||
|
float acc = 0.0;
|
||||||
|
[loop] for (int j = 0; j < n; ++j) {
|
||||||
|
float r = sqrt((float(j) + 0.5) / float(n));
|
||||||
|
float a = float(j) * 2.399963;
|
||||||
|
float2 o = float2(cos(a), sin(a)) * r;
|
||||||
|
float d = NsmTap(suv + o * rfilt, refd, tile_u0);
|
||||||
|
acc += saturate((d - refd) / w + 0.5);
|
||||||
|
}
|
||||||
|
float s = acc / float(n);
|
||||||
|
return 1.0 - fade * nsm_p.x * (1.0 - s);
|
||||||
|
}
|
||||||
// Screen-space UV-gradient tangent frame (cotangent reconstruction from the
|
// Screen-space UV-gradient tangent frame (cotangent reconstruction from the
|
||||||
// position/uv derivatives; signs/lengths calibrated against the meshes' REAL
|
// position/uv derivatives; signs/lengths calibrated against the meshes' REAL
|
||||||
// stored TBN; see the env-family branch notes). The RAW screen frame is
|
// stored TBN; see the env-family branch notes). The RAW screen frame is
|
||||||
|
|
@ -435,7 +737,8 @@ float4 ps_main(VSOut i) : SV_Target {
|
||||||
// CSM shadow (shared receiver rows at t7). extra_bias -1 = the game's
|
// CSM shadow (shared receiver rows at t7). extra_bias -1 = the game's
|
||||||
// own per-cascade dynamicobject receive bias (props are casters;
|
// own per-cascade dynamicobject receive bias (props are casters;
|
||||||
// without it their flat tops self-shadow and flicker).
|
// without it their flat tops self-shadow and flicker).
|
||||||
float s = SampleCsmShadow(i.rpos + cam_pos.xyz, -1.0);
|
float s = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, -1.0, wn, i.pos.xy),
|
||||||
|
SampleStaticSun(i.rpos + cam_pos.xyz, wn, i.pos.xy));
|
||||||
// Static world-shadow term: the game's baked building/tree shade,
|
// Static world-shadow term: the game's baked building/tree shade,
|
||||||
// sampled from the natively re-rendered map at t4 (flag 8) with the
|
// sampled from the natively re-rendered map at t4 (flag 8) with the
|
||||||
// PS's own 4-tap PCF (point taps at +-0.5 texels, manual bilinear:
|
// PS's own 4-tap PCF (point taps at +-0.5 texels, manual bilinear:
|
||||||
|
|
@ -606,7 +909,8 @@ float4 ps_main(VSOut i) : SV_Target {
|
||||||
// tens of cm). Without this the held skateboard, a big flat surface
|
// tens of cm). Without this the held skateboard, a big flat surface
|
||||||
// that is almost always inside the skater's own shadow, renders
|
// that is almost always inside the skater's own shadow, renders
|
||||||
// fully sunlit (near-white) against the emulated dark deck.
|
// fully sunlit (near-white) against the emulated dark deck.
|
||||||
float csm = SampleCsmShadow(i.rpos + cam_pos.xyz, 0.012);
|
float csm = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, 0.012, cn, i.pos.xy),
|
||||||
|
SampleStaticSun(i.rpos + cam_pos.xyz, cn, i.pos.xy));
|
||||||
float3 sel = dlin.g > 0.001225
|
float3 sel = dlin.g > 0.001225
|
||||||
? dlin
|
? dlin
|
||||||
: ch_tintA.rgb * dlin.r + ch_tintB.rgb * dlin.b;
|
: ch_tintA.rgb * dlin.r + ch_tintB.rgb * dlin.b;
|
||||||
|
|
@ -618,7 +922,8 @@ float4 ps_main(VSOut i) : SV_Target {
|
||||||
} else {
|
} else {
|
||||||
// defaultcharacter / CAC pieces: key light + SH irradiance ambient,
|
// defaultcharacter / CAC pieces: key light + SH irradiance ambient,
|
||||||
// key gated by the CSM shadow (see the livingworld comment above).
|
// key gated by the CSM shadow (see the livingworld comment above).
|
||||||
float csm = SampleCsmShadow(i.rpos + cam_pos.xyz, 0.012);
|
float csm = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, 0.012, cn, i.pos.xy),
|
||||||
|
SampleStaticSun(i.rpos + cam_pos.xyz, cn, i.pos.xy));
|
||||||
if (ch_tintA.w > 0.0) {
|
if (ch_tintA.w > 0.0) {
|
||||||
dlin *= ch_tintA.rgb;
|
dlin *= ch_tintA.rgb;
|
||||||
}
|
}
|
||||||
|
|
@ -698,7 +1003,8 @@ float4 ps_main(VSOut i) : SV_Target {
|
||||||
}
|
}
|
||||||
float3 wn = normalize(vn.x * wt + vn.y * wb + vn.z * wn0);
|
float3 wn = normalize(vn.x * wt + vn.y * wb + vn.z * wn0);
|
||||||
float3 vd = -normalize(i.rpos);
|
float3 vd = -normalize(i.rpos);
|
||||||
float s = SampleCsmShadow(i.rpos + cam_pos.xyz, 0.0);
|
float s = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, 0.0, wn, i.pos.xy),
|
||||||
|
SampleStaticSun(i.rpos + cam_pos.xyz, wn, i.pos.xy));
|
||||||
// Lightmap at the unwrap + 0.01 * vNormal.xz (the ripple refraction
|
// Lightmap at the unwrap + 0.01 * vNormal.xz (the ripple refraction
|
||||||
// wobble), console fetch semantics, min-clamped like every world
|
// wobble), console fetch semantics, min-clamped like every world
|
||||||
// family. White-fallback pages skip the clamp (see the env fetch).
|
// family. White-fallback pages skip the clamp (see the env fetch).
|
||||||
|
|
@ -849,27 +1155,11 @@ float4 ps_main(VSOut i) : SV_Target {
|
||||||
}
|
}
|
||||||
float3 dcol = dlin * ov;
|
float3 dcol = dlin * ov;
|
||||||
// CSM shadow term s = sat(infront + 1 - coverage) from the native
|
// CSM shadow term s = sat(infront + 1 - coverage) from the native
|
||||||
// atlas (same cascade select as the legacy receive path).
|
// atlas (SampleCsmShadow bias/cascade conventions; the soft variant
|
||||||
float s = 1.0;
|
// adds the contact-hardening filter when PCSS is enabled), min'd
|
||||||
if (sh_misc.y > 0.0) {
|
// with the native static sun-shadow term.
|
||||||
float3 wp = i.rpos + cam_pos.xyz;
|
float s = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, 0.0, i.nrm, i.pos.xy),
|
||||||
float2 lsv =
|
SampleStaticSun(i.rpos + cam_pos.xyz, i.nrm, i.pos.xy));
|
||||||
float2(dot(sh_x.xyz, wp) + sh_x.w, dot(sh_y.xyz, wp) + sh_y.w);
|
|
||||||
float rd = dot(sh_z.xyz, wp) + sh_z.w - sh_misc.x;
|
|
||||||
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) {
|
|
||||||
float2 suv = float2(luv.x / 6.0 + (casc * 2.0 - 1.0) / 6.0,
|
|
||||||
luv.y * -0.5 + 0.5);
|
|
||||||
float2 sm2 = shadow_atlas.Sample(smp_clamp, suv);
|
|
||||||
s = saturate((sm2.x >= rd ? 1.0 : 0.0) + (1.0 - sm2.y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Lightmap fetch = the console's semantics: BILINEAR, CLAMPED, mip 0.
|
// Lightmap fetch = the console's semantics: BILINEAR, CLAMPED, mip 0.
|
||||||
// The composed atlas pages are single-level on console and the fetch
|
// The composed atlas pages are single-level on console and the fetch
|
||||||
// constants carry clamp_x/clamp_y = 2. Sampling them with the aniso-8
|
// constants carry clamp_x/clamp_y = 2. Sampling them with the aniso-8
|
||||||
|
|
@ -1160,22 +1450,10 @@ float4 ps_main(VSOut i) : SV_Target {
|
||||||
// per channel. A fixed-denominator curve here read pitch-black and
|
// per channel. A fixed-denominator curve here read pitch-black and
|
||||||
// double-darkened baked shade.
|
// double-darkened baked shade.
|
||||||
if (sh_misc.y > 0.0 && tint.g == 0.0 && tint.b == 0.0 && misc.x == 0.0) {
|
if (sh_misc.y > 0.0 && tint.g == 0.0 && tint.b == 0.0 && misc.x == 0.0) {
|
||||||
float3 wp = i.rpos + cam_pos.xyz;
|
// Uncovered positions return 1 and the min-clamp is a no-op there.
|
||||||
float2 lsv = float2(dot(sh_x.xyz, wp) + sh_x.w, dot(sh_y.xyz, wp) + sh_y.w);
|
float s = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, 0.0, i.nrm, i.pos.xy),
|
||||||
float rd = dot(sh_z.xyz, wp) + sh_z.w - sh_misc.x;
|
SampleStaticSun(i.rpos + cam_pos.xyz, i.nrm, i.pos.xy));
|
||||||
float2 luv = 0.0;
|
light = min(light, sqrt(s + sh_color.rgb));
|
||||||
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) {
|
|
||||||
float2 uv = float2(luv.x / 6.0 + (casc * 2.0 - 1.0) / 6.0, luv.y * -0.5 + 0.5);
|
|
||||||
float2 m = shadow_atlas.Sample(smp_clamp, uv);
|
|
||||||
float s = saturate((m.x >= rd ? 1.0 : 0.0) + (1.0 - m.y));
|
|
||||||
light = min(light, sqrt(s + sh_color.rgb));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
float3 lit = albedo.rgb * light;
|
float3 lit = albedo.rgb * light;
|
||||||
if (mat_tint.w > 0.0 && misc.x == 0.0) {
|
if (mat_tint.w > 0.0 && misc.x == 0.0) {
|
||||||
|
|
@ -1396,3 +1674,17 @@ float4 ps_refl_gbuf(VSOut i) : SV_Target {
|
||||||
float2 ps_shadow_caster(VSOut i) : SV_Target {
|
float2 ps_shadow_caster(VSOut i) : SV_Target {
|
||||||
return float2(i.pos.z, 0.0);
|
return float2(i.pos.z, 0.0);
|
||||||
}
|
}
|
||||||
|
// Alpha-tested caster variant (foliage cards, alphatest fences/grates in
|
||||||
|
// the static sun-shadow map; dynamicobject.alphatest props in the CSM
|
||||||
|
// atlas): sampling the item's diffuse keeps the cutout silhouette instead
|
||||||
|
// of casting the full card quad. ALPHAREF 30/255 matches the world
|
||||||
|
// families' own opaque alpha test.
|
||||||
|
float2 ps_shadow_caster_clip(VSOut i) : SV_Target {
|
||||||
|
// +2 LOD bias: at the static sun map's ~2 cm near-cascade texels the
|
||||||
|
// raw leaf cutout rasters as binary salt-and-pepper dapple; two mip
|
||||||
|
// levels of alpha pre-filtering merge it into the soft blobs the baked
|
||||||
|
// lighting shows. The low ALPHAREF keeps mip-averaged coverage from
|
||||||
|
// eroding the canopy.
|
||||||
|
clip(diffuse.SampleBias(smp, i.uv, 2.0).a - 0.1176);
|
||||||
|
return float2(i.pos.z, 0.0);
|
||||||
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -59,6 +59,13 @@ REXCVAR_DECLARE(bool, skate3_native_render_scene_ssao);
|
||||||
REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_radius);
|
REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_radius);
|
||||||
REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_intensity);
|
REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_intensity);
|
||||||
REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_luma_protect);
|
REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_luma_protect);
|
||||||
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_shadow_static_casters);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_static_strength);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_static_radius);
|
||||||
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_shadow_pcss);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_pcss_sun_deg);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_pcss_max_m);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_static_bias);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_sun_override);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_sun_override);
|
||||||
REXCVAR_DECLARE(double, skate3_native_render_scene_sun_azimuth);
|
REXCVAR_DECLARE(double, skate3_native_render_scene_sun_azimuth);
|
||||||
REXCVAR_DECLARE(double, skate3_native_render_scene_sun_elevation);
|
REXCVAR_DECLARE(double, skate3_native_render_scene_sun_elevation);
|
||||||
|
|
@ -294,6 +301,59 @@ void NativeDebugDialog::OnDraw(ImGuiIO& io) {
|
||||||
0.0f, 3.0f, "%.2f",
|
0.0f, 3.0f, "%.2f",
|
||||||
"How strongly bright (sun-lit) surfaces resist SSAO "
|
"How strongly bright (sun-lit) surfaces resist SSAO "
|
||||||
"darkening (ambient-only approximation)."));
|
"darkening (ambient-only approximation)."));
|
||||||
|
REXCVAR_SET(skate3_native_render_scene_shadow_static_casters,
|
||||||
|
CvarCheckbox(
|
||||||
|
"Static sun shadows",
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_shadow_static_casters),
|
||||||
|
"Native static sun-shadow map: buildings, trees, rails and "
|
||||||
|
"placed props cast live shadows from a dedicated "
|
||||||
|
"sun-aligned depth map: shade on characters and props, "
|
||||||
|
"and shadows that follow a moved sun. The game's own "
|
||||||
|
"dynamic shadow cascades are untouched."));
|
||||||
|
REXCVAR_SET(
|
||||||
|
skate3_native_render_scene_shadow_static_strength,
|
||||||
|
CvarSlider(
|
||||||
|
"static shadow strength",
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_shadow_static_strength),
|
||||||
|
0.0f, 1.0f, "%.2f",
|
||||||
|
"How dark static-geometry shadows get (characters/props always "
|
||||||
|
"cast at full strength). Lower if live static shade fights the "
|
||||||
|
"baked lighting."));
|
||||||
|
REXCVAR_SET(
|
||||||
|
skate3_native_render_scene_shadow_static_radius,
|
||||||
|
CvarSlider(
|
||||||
|
"static shadow radius (m)",
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_shadow_static_radius), 40.0f,
|
||||||
|
600.0f, "%.0f",
|
||||||
|
"Far-cascade half-extent of the static sun-shadow map (mid and "
|
||||||
|
"inner cascades cover 1/2 and 1/6 at 2x/6x density). Larger "
|
||||||
|
"reaches farther at lower far-cascade density."));
|
||||||
|
REXCVAR_SET(skate3_native_render_scene_shadow_pcss,
|
||||||
|
CvarCheckbox(
|
||||||
|
"Soft shadows (PCSS)",
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_shadow_pcss),
|
||||||
|
"Contact-hardening filter: crisp where a shadow touches "
|
||||||
|
"its caster, progressively softer with caster height."));
|
||||||
|
REXCVAR_SET(skate3_native_render_scene_shadow_pcss_sun_deg,
|
||||||
|
CvarSlider(
|
||||||
|
"sun angular size (deg)",
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_shadow_pcss_sun_deg),
|
||||||
|
0.1f, 8.0f, "%.1f",
|
||||||
|
"Penumbra growth per meter of caster height (the real sun "
|
||||||
|
"is ~0.53 deg)."));
|
||||||
|
REXCVAR_SET(skate3_native_render_scene_shadow_pcss_max_m,
|
||||||
|
CvarSlider(
|
||||||
|
"max penumbra (m)",
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_shadow_pcss_max_m),
|
||||||
|
0.05f, 5.0f, "%.2f"));
|
||||||
|
REXCVAR_SET(
|
||||||
|
skate3_native_render_scene_shadow_static_bias,
|
||||||
|
CvarSlider("static receive bias (m)",
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_shadow_static_bias),
|
||||||
|
0.0f, 0.5f, "%.3f",
|
||||||
|
"Raise if static geometry shows self-shadow acne (stipple "
|
||||||
|
"on sunlit ground/walls); lower if static shadows visibly "
|
||||||
|
"detach from their casters."));
|
||||||
{
|
{
|
||||||
const bool was_on = REXCVAR_GET(skate3_native_render_scene_sun_override);
|
const bool was_on = REXCVAR_GET(skate3_native_render_scene_sun_override);
|
||||||
const bool now_on =
|
const bool now_on =
|
||||||
|
|
|
||||||
|
|
@ -604,6 +604,87 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_shadow_caster_parity, true,
|
||||||
"casting it painted a hard brim band across the editor face). "
|
"casting it painted a hard brim band across the editor face). "
|
||||||
"OFF = every published character piece casts (old behavior).")
|
"OFF = every published character piece casts (old behavior).")
|
||||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_shadow_static_casters, true,
|
||||||
|
"Skate 3",
|
||||||
|
"Native static sun-shadow map: render the STATIC world "
|
||||||
|
"(buildings, trees, rails, placed props) into a separate "
|
||||||
|
"camera-centered ortho depth map along the material sun, "
|
||||||
|
"sampled by every lit surface in addition to the game's "
|
||||||
|
"dynamic cascades. Live static shade on characters and "
|
||||||
|
"props (which baked lightmaps cannot shade) and shadows "
|
||||||
|
"that follow a moved sun; receivers keep the darker of "
|
||||||
|
"baked and live shade rather than doubling.")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_shadow_static_strength, 1.0,
|
||||||
|
"Skate 3",
|
||||||
|
"How dark static-geometry shadows get (1 = the full "
|
||||||
|
"dynamic-shadow clamp, 0 = invisible). Applied at "
|
||||||
|
"receive from the static sun map, so characters and "
|
||||||
|
"props keep full-strength shadows. Moderate values "
|
||||||
|
"keep live static shade believable where it disagrees "
|
||||||
|
"with the baked lighting's own sun.")
|
||||||
|
.range(0.0, 1.0)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_shadow_static_radius, 180.0,
|
||||||
|
"Skate 3",
|
||||||
|
"Half-extent in meters of the static sun-shadow map's "
|
||||||
|
"far cascade (camera-centered; the mid and inner "
|
||||||
|
"cascades cover 1/2 and 1/6 of this at 2x and 6x "
|
||||||
|
"the texel density). Larger reaches farther at lower "
|
||||||
|
"texel density; the term fades out at the outer edge.")
|
||||||
|
.range(40.0, 600.0)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_INT32(skate3_native_render_scene_shadow_static_size, 4096,
|
||||||
|
"Skate 3",
|
||||||
|
"Static sun-shadow map resolution per cascade tile "
|
||||||
|
"(the map is three tiles). At the default radius the "
|
||||||
|
"inner cascade gets ~1.5 cm texels, mid ~4 cm, far "
|
||||||
|
"~9 cm.")
|
||||||
|
.range(1024, 8192)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
|
||||||
|
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_shadow_pcss, true, "Skate 3",
|
||||||
|
"Contact-hardening soft shadows (PCSS): a blocker search "
|
||||||
|
"estimates the caster distance per pixel and the filter "
|
||||||
|
"width follows the sun's angular size: crisp at the "
|
||||||
|
"contact point, progressively softer with caster height. "
|
||||||
|
"OFF = the game's fixed-width blurred atlas sampling.")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_shadow_pcss_sun_deg, 2.5,
|
||||||
|
"Skate 3",
|
||||||
|
"Sun angular diameter in degrees for the PCSS penumbra "
|
||||||
|
"(the real sun is ~0.53; larger reads softer).")
|
||||||
|
.range(0.1, 8.0)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_shadow_pcss_max_m, 0.8,
|
||||||
|
"Skate 3",
|
||||||
|
"Maximum PCSS penumbra half-width in meters (caps how "
|
||||||
|
"soft very tall casters can get).")
|
||||||
|
.range(0.05, 5.0)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_shadow_pcss_blocker_m, 2.5,
|
||||||
|
"Skate 3",
|
||||||
|
"PCSS blocker-search radius in meters. Must be at "
|
||||||
|
"least the max penumbra for distant casters to soften "
|
||||||
|
"fully; larger values let unrelated nearby casters "
|
||||||
|
"soften edges they should not.")
|
||||||
|
.range(0.1, 8.0)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_shadow_pcss_min_texel, 1.0,
|
||||||
|
"Skate 3",
|
||||||
|
"Minimum PCSS filter radius in physical atlas texels "
|
||||||
|
"(keeps fully hardened edges filtered).")
|
||||||
|
.range(0.0, 4.0)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_shadow_static_bias, 0.08,
|
||||||
|
"Skate 3",
|
||||||
|
"Static sun-shadow receiver bias in METERS; static "
|
||||||
|
"geometry is its own caster, so its surfaces compare "
|
||||||
|
"against their own map depth. Slope-scaled: grazing "
|
||||||
|
"sun angles get up to several times this. Raise on "
|
||||||
|
"acne (stipple on sunlit ground/walls), lower if "
|
||||||
|
"static shadows visibly detach from their casters.")
|
||||||
|
.range(0.0, 0.5)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_char_shadow_exact, true,
|
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_char_shadow_exact, true,
|
||||||
"Skate 3",
|
"Skate 3",
|
||||||
"Characters sample the CSM atlas with the game's own math: "
|
"Characters sample the CSM atlas with the game's own math: "
|
||||||
|
|
@ -7276,25 +7357,18 @@ static void RebasisLightRow(float* row, const float axis[3],
|
||||||
row[3] = at_cam - (row[0] * cam[0] + row[1] * cam[1] + row[2] * cam[2]);
|
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):
|
// Rebuild the frame's captured shadow transforms around the given unit
|
||||||
// rotates every captured per-frame light transform in the published scene
|
// direction TOWARD the sun: the light basis rows via RebasisLightRow
|
||||||
// to the azimuth/elevation-driven direction. The native CSM caster pass,
|
// (preserving each row's scale and its value at the camera, so cascade
|
||||||
// the material receivers, the static world-shadow map (which re-primes
|
// centering and bias conventions carry over) plus a camera re-center of
|
||||||
// automatically when its rows change) and the volumetric shafts all read
|
// the cascade sub-boxes (c1/c2 scale.xy + offset.zw); the game fits their
|
||||||
// these rows, so the whole dynamic lighting stack follows the moved sun.
|
// offsets for ITS axis each frame, and under a rotated basis the finest
|
||||||
// Baked lightmap shade and the sky dome's painted sun are game content and
|
// cascade can drift off the player (the shadow then falls back to the
|
||||||
// stay put.
|
// coarser tiles, visibly blocky). Caster pass and receivers share these
|
||||||
static void ApplySunOverride(FrameScene& scene) {
|
// rows, so the re-fit stays self-consistent. Shared by the lighting-lab
|
||||||
if (!REXCVAR_GET(skate3_native_render_scene_sun_override)) {
|
// sun override and the static-caster basis alignment below.
|
||||||
return;
|
static void RebasisShadowRows(FrameScene& scene, const float sun[3],
|
||||||
}
|
bool include_ws) {
|
||||||
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
|
// Light basis: depth axis points away from the sun; X/Y span the shadow
|
||||||
// plane. Handedness is irrelevant; casters and receivers share the rows.
|
// plane. Handedness is irrelevant; casters and receivers share the rows.
|
||||||
const float zl[3] = {-sun[0], -sun[1], -sun[2]};
|
const float zl[3] = {-sun[0], -sun[1], -sun[2]};
|
||||||
|
|
@ -7315,15 +7389,6 @@ static void ApplySunOverride(FrameScene& scene) {
|
||||||
RebasisLightRow(scene.shadow_rows + 0, xl, cam); // c0 light-space X
|
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 + 12, yl, cam); // c3 light-space Y
|
||||||
RebasisLightRow(scene.shadow_rows + 16, zl, cam); // c4 depth ramp
|
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* rows = scene.shadow_rows;
|
||||||
const float lsx =
|
const float lsx =
|
||||||
rows[0] * cam[0] + rows[1] * cam[1] + rows[2] * cam[2] + rows[3];
|
rows[0] * cam[0] + rows[1] * cam[1] + rows[2] * cam[2] + rows[3];
|
||||||
|
|
@ -7334,16 +7399,43 @@ static void ApplySunOverride(FrameScene& scene) {
|
||||||
scene.shadow_rows[10] = -lsx * scene.shadow_rows[8]; // c2 offset
|
scene.shadow_rows[10] = -lsx * scene.shadow_rows[8]; // c2 offset
|
||||||
scene.shadow_rows[11] = -lsy * scene.shadow_rows[9];
|
scene.shadow_rows[11] = -lsy * scene.shadow_rows[9];
|
||||||
}
|
}
|
||||||
|
if (include_ws && 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)};
|
||||||
|
RebasisShadowRows(scene, sun, /*include_ws=*/true);
|
||||||
|
if (scene.shadow_valid) {
|
||||||
|
scene.shadow_rows[24] = sun[0]; // c6 sun direction
|
||||||
|
scene.shadow_rows[25] = sun[1];
|
||||||
|
scene.shadow_rows[26] = sun[2];
|
||||||
|
}
|
||||||
if (scene.dynobj_valid) {
|
if (scene.dynobj_valid) {
|
||||||
scene.dynobj_rows[0] = sun[0];
|
scene.dynobj_rows[0] = sun[0];
|
||||||
scene.dynobj_rows[1] = sun[1];
|
scene.dynobj_rows[1] = sun[1];
|
||||||
scene.dynobj_rows[2] = sun[2];
|
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) {
|
if (scene.sky_sun_valid) {
|
||||||
scene.sky_sun[0] = sun[0];
|
scene.sky_sun[0] = sun[0];
|
||||||
scene.sky_sun[1] = sun[1];
|
scene.sky_sun[1] = sun[1];
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include <bit>
|
#include <bit>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
@ -94,6 +95,16 @@ REXCVAR_DECLARE(bool, skate3_native_render_scene_ropa_blend);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_ropa_inline);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_ropa_inline);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_selection_outline);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_selection_outline);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_shadow_caster_parity);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_shadow_caster_parity);
|
||||||
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_shadow_pcss);
|
||||||
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_shadow_static_casters);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_pcss_blocker_m);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_pcss_max_m);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_pcss_min_texel);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_pcss_sun_deg);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_static_bias);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_static_radius);
|
||||||
|
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_shadow_static_size);
|
||||||
|
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_static_strength);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_shadows);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_shadows);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_sort_opaque);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_sort_opaque);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_splines);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_splines);
|
||||||
|
|
@ -2369,9 +2380,12 @@ bool EnsureRootSignature(const NativeGuestOutputRenderContext& context) {
|
||||||
ld.params[7] = {nrhi::BindingParamKind::kTextureTable, 6, 2,
|
ld.params[7] = {nrhi::BindingParamKind::kTextureTable, 6, 2,
|
||||||
nrhi::Visibility::kPixel};
|
nrhi::Visibility::kPixel};
|
||||||
// World-shading v2 material maps: the detail normal map (t8) + the
|
// World-shading v2 material maps: the detail normal map (t8) + the
|
||||||
// decal families' spec/ecc masks (t9), bound as a pair on draws whose
|
// decal families' spec/ecc masks (t9), plus the native static
|
||||||
// misc.z flags enable them.
|
// sun-shadow map (t10) as the table's third entry; extending an
|
||||||
ld.params[8] = {nrhi::BindingParamKind::kTextureTable, 8, 2,
|
// existing table costs no root-signature DWORDs. Bound together via
|
||||||
|
// SetTextures (a pair-only bind would drop t10 to the backend
|
||||||
|
// fallback).
|
||||||
|
ld.params[8] = {nrhi::BindingParamKind::kTextureTable, 8, 3,
|
||||||
nrhi::Visibility::kPixel};
|
nrhi::Visibility::kPixel};
|
||||||
// Character lighting block (b2): the canonical per-draw rows captured
|
// Character lighting block (b2): the canonical per-draw rows captured
|
||||||
// from the guest PS bank (CaptureCharLighting), sliced out of the bone
|
// from the guest PS bank (CaptureCharLighting), sliced out of the bone
|
||||||
|
|
@ -2851,13 +2865,17 @@ bool EnsureShadowPsos(const NativeGuestOutputRenderContext& context) {
|
||||||
nrhi::Shader* cps = device->CreateShader(
|
nrhi::Shader* cps = device->CreateShader(
|
||||||
MakeShaderDesc(nrhi::ShaderStage::kPixel, "scene.hlsl", kShaderSource,
|
MakeShaderDesc(nrhi::ShaderStage::kPixel, "scene.hlsl", kShaderSource,
|
||||||
"ps_shadow_caster", nullptr, ""));
|
"ps_shadow_caster", nullptr, ""));
|
||||||
|
nrhi::Shader* cpc = device->CreateShader(
|
||||||
|
MakeShaderDesc(nrhi::ShaderStage::kPixel, "scene.hlsl", kShaderSource,
|
||||||
|
"ps_shadow_caster_clip", nullptr, ""));
|
||||||
nrhi::Shader* bvs = device->CreateShader(
|
nrhi::Shader* bvs = device->CreateShader(
|
||||||
MakeShaderDesc(nrhi::ShaderStage::kVertex, "shadow_blur.hlsl",
|
MakeShaderDesc(nrhi::ShaderStage::kVertex, "shadow_blur.hlsl",
|
||||||
kShadowBlurSource, "vs_main", nullptr, ""));
|
kShadowBlurSource, "vs_main", nullptr, ""));
|
||||||
nrhi::Shader* bps = device->CreateShader(
|
nrhi::Shader* bps = device->CreateShader(
|
||||||
MakeShaderDesc(nrhi::ShaderStage::kPixel, "shadow_blur.hlsl",
|
MakeShaderDesc(nrhi::ShaderStage::kPixel, "shadow_blur.hlsl",
|
||||||
kShadowBlurSource, "ps_main", nullptr, ""));
|
kShadowBlurSource, "ps_main", nullptr, ""));
|
||||||
if (cvs == nullptr || cps == nullptr || bvs == nullptr || bps == nullptr) {
|
if (cvs == nullptr || cps == nullptr || cpc == nullptr || bvs == nullptr ||
|
||||||
|
bps == nullptr) {
|
||||||
REXLOG_ERROR("native-scene: shadow shader compile failed");
|
REXLOG_ERROR("native-scene: shadow shader compile failed");
|
||||||
g_r.failed = true;
|
g_r.failed = true;
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -2882,6 +2900,8 @@ bool EnsureShadowPsos(const NativeGuestOutputRenderContext& context) {
|
||||||
cp.rtv_format = nrhi::Format::kR16G16_UNORM;
|
cp.rtv_format = nrhi::Format::kR16G16_UNORM;
|
||||||
cp.sample_count = 1;
|
cp.sample_count = 1;
|
||||||
g_r.pso_shadow_caster = device->CreateGraphicsPipeline(cp);
|
g_r.pso_shadow_caster = device->CreateGraphicsPipeline(cp);
|
||||||
|
cp.ps = cpc;
|
||||||
|
g_r.pso_shadow_caster_clip = device->CreateGraphicsPipeline(cp);
|
||||||
nrhi::GraphicsPipelineDesc bp;
|
nrhi::GraphicsPipelineDesc bp;
|
||||||
bp.layout = g_r.layout;
|
bp.layout = g_r.layout;
|
||||||
bp.vs = bvs;
|
bp.vs = bvs;
|
||||||
|
|
@ -2893,9 +2913,12 @@ bool EnsureShadowPsos(const NativeGuestOutputRenderContext& context) {
|
||||||
g_r.pso_shadow_blur = device->CreateGraphicsPipeline(bp);
|
g_r.pso_shadow_blur = device->CreateGraphicsPipeline(bp);
|
||||||
device->DestroyDeferred(cvs);
|
device->DestroyDeferred(cvs);
|
||||||
device->DestroyDeferred(cps);
|
device->DestroyDeferred(cps);
|
||||||
|
device->DestroyDeferred(cpc);
|
||||||
device->DestroyDeferred(bvs);
|
device->DestroyDeferred(bvs);
|
||||||
device->DestroyDeferred(bps);
|
device->DestroyDeferred(bps);
|
||||||
if (g_r.pso_shadow_caster == nullptr || g_r.pso_shadow_blur == nullptr) {
|
if (g_r.pso_shadow_caster == nullptr ||
|
||||||
|
g_r.pso_shadow_caster_clip == nullptr ||
|
||||||
|
g_r.pso_shadow_blur == nullptr) {
|
||||||
REXLOG_ERROR("native-scene: shadow PSO creation failed");
|
REXLOG_ERROR("native-scene: shadow PSO creation failed");
|
||||||
g_r.failed = true;
|
g_r.failed = true;
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -3241,6 +3264,39 @@ bool EnsureShadowResources(const NativeGuestOutputRenderContext& context) {
|
||||||
g_r.world_shadow_in_srv = false;
|
g_r.world_shadow_in_srv = false;
|
||||||
g_r.world_shadow_primed = false;
|
g_r.world_shadow_primed = false;
|
||||||
}
|
}
|
||||||
|
if (!g_r.static_sun && g_r.shadow_raw != nullptr &&
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_shadow_static_casters)) {
|
||||||
|
// Native static sun-shadow map (see RendererState). THREE cascade
|
||||||
|
// tiles side by side: inner (r/6, centimeter contact detail with
|
||||||
|
// useful reach), mid (r/2) and far (full radius, large-caster
|
||||||
|
// coverage); size is per tile, restart-scoped.
|
||||||
|
const int32_t size_cfg =
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_shadow_static_size);
|
||||||
|
g_r.static_sun_size = uint32_t(std::clamp(size_cfg, 1024, 8192));
|
||||||
|
nrhi::TextureDesc desc;
|
||||||
|
desc.width = g_r.static_sun_size * 3;
|
||||||
|
desc.height = g_r.static_sun_size;
|
||||||
|
desc.format = nrhi::Format::kR16G16_UNORM;
|
||||||
|
desc.usage = nrhi::kTextureUsageRenderTarget;
|
||||||
|
desc.initial_state = nrhi::ResourceState::kRenderTarget;
|
||||||
|
desc.clear_color[0] = 1.0f; // depth: far = lit
|
||||||
|
desc.clear_color[1] = 1.0f;
|
||||||
|
g_r.static_sun = device->CreateTexture(desc);
|
||||||
|
if (g_r.static_sun != nullptr) {
|
||||||
|
nrhi::TextureViewDesc vd;
|
||||||
|
vd.mip_levels = 1;
|
||||||
|
g_r.static_sun_srv = device->CreateTextureView(g_r.static_sun, vd);
|
||||||
|
}
|
||||||
|
if (g_r.static_sun_srv == nullptr) {
|
||||||
|
REXLOG_ERROR("native-scene: static sun-shadow map creation failed");
|
||||||
|
g_r.failed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_r.static_sun_in_srv = false;
|
||||||
|
REXLOG_INFO(
|
||||||
|
"native-scene: static sun-shadow map created ({}x{}, 3 cascades)",
|
||||||
|
g_r.static_sun_size * 3, g_r.static_sun_size);
|
||||||
|
}
|
||||||
if (!g_r.shadow_cb) {
|
if (!g_r.shadow_cb) {
|
||||||
// Always created (even with shadows off): the scene PS declares b1 and
|
// Always created (even with shadows off): the scene PS declares b1 and
|
||||||
// a root CBV must be bound; a zeroed block disables the shadow branch.
|
// a root CBV must be bound; a zeroed block disables the shadow branch.
|
||||||
|
|
@ -4663,6 +4719,15 @@ bool YieldForMenus(const NativeGuestOutputRenderContext& context) {
|
||||||
ClearItemCache();
|
ClearItemCache();
|
||||||
// Off-screen retention holds guest-address-keyed copies too.
|
// Off-screen retention holds guest-address-keyed copies too.
|
||||||
g_retained_clear.store(true, std::memory_order_relaxed);
|
g_retained_clear.store(true, std::memory_order_relaxed);
|
||||||
|
// The static-caster cache holds world-positioned records from the
|
||||||
|
// OUTGOING map; different maps share coordinate ranges, so stale
|
||||||
|
// records whose meshes survive the transition would keep casting
|
||||||
|
// (an entire arriving area sat under the previous map's building
|
||||||
|
// shadows). Invalidate the sun map with it.
|
||||||
|
g_r.static_casters.clear();
|
||||||
|
g_r.nsm_dirty = true;
|
||||||
|
g_r.static_sun_valid = false;
|
||||||
|
g_r.nsm_built_radius = 0.0f;
|
||||||
std::lock_guard<std::mutex> lock(g_prewarm_mutex);
|
std::lock_guard<std::mutex> lock(g_prewarm_mutex);
|
||||||
g_prewarm_seen.clear();
|
g_prewarm_seen.clear();
|
||||||
g_prewarm_tex_seen.clear();
|
g_prewarm_tex_seen.clear();
|
||||||
|
|
@ -5428,6 +5493,361 @@ void ReleaseRetiredAndFlushCaches(const NativeGuestOutputRenderContext& context)
|
||||||
// three cascade tiles with the captured light rows, then applies the
|
// three cascade tiles with the captured light rows, then applies the
|
||||||
// game's coverage blur + depth dilation. Runs before the main pass so the
|
// game's coverage blur + depth dilation. Runs before the main pass so the
|
||||||
// scene shader can sample the finished atlas.
|
// scene shader can sample the finished atlas.
|
||||||
|
// Read-only resolved-texture lookup (route -> store entry) for the caster
|
||||||
|
// passes' alpha-test binding: the main pass owns decode scheduling and
|
||||||
|
// live guest reads; an unresolved diffuse just casts opaque for the frames
|
||||||
|
// a first-sight decode takes.
|
||||||
|
static nrhi::TextureView* LookupResolvedTexture(uint32_t tex_ptr) {
|
||||||
|
if (tex_ptr == 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto rit = g_r.tex_routes.find(tex_ptr);
|
||||||
|
if (rit == g_r.tex_routes.end()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto sit = g_r.tex_store.find(rit->second.key);
|
||||||
|
if (sit == g_r.tex_store.end() || !sit->second.valid) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return sit->second.srv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native static sun-shadow map: one camera-centered ortho depth pass over
|
||||||
|
// the STATIC world items along the material sun (the captured c6 row:
|
||||||
|
// the direction the world materials and their baked shadows are lit
|
||||||
|
// from). The game's own cascade transforms are a stylized near-vertical
|
||||||
|
// projection fit to a ~12 m height window (measured ~43 degrees off the
|
||||||
|
// material sun): adequate for the dynamic casters they were built for,
|
||||||
|
// but tall static geometry projected through them shadows its own
|
||||||
|
// plan-view footprint. This map gives static shade a true sun axis, a
|
||||||
|
// full-range depth window (no clamp blobs) and its own resolution;
|
||||||
|
// receivers min it with the CSM term (SampleStaticSun), bottoming out at
|
||||||
|
// the configured strength. Re-rendered every frame; the box follows the
|
||||||
|
// camera, with the origin snapped to the texel grid so the static raster
|
||||||
|
// never swims sub-texel (foliage shimmer).
|
||||||
|
static void RenderStaticSunMap(const NativeGuestOutputRenderContext& context,
|
||||||
|
const FrameScene& scene, uint32_t bone_region,
|
||||||
|
int32_t debug_mode, uint64_t frame_number) {
|
||||||
|
g_r.static_sun_valid = false;
|
||||||
|
if (g_r.static_sun == nullptr || g_r.pso_shadow_caster == nullptr ||
|
||||||
|
g_r.pso_shadow_caster_clip == nullptr || debug_mode != 0 ||
|
||||||
|
!scene.shadow_valid ||
|
||||||
|
!REXCVAR_GET(skate3_native_render_scene_shadows) ||
|
||||||
|
!REXCVAR_GET(skate3_native_render_scene_shadow_static_casters)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Sun axis = the material sun (c6). It is the only sun-like direction
|
||||||
|
// in the captured data: the game's cascade transforms AND its baked
|
||||||
|
// static-shade (world-shadow) projection are both the same stylized
|
||||||
|
// near-vertical axis, measured ~43 degrees off c6, projecting statics
|
||||||
|
// along either shadows their plan-view footprint. Shadows cast along c6
|
||||||
|
// are geometrically consistent with the materials' lighting; where the
|
||||||
|
// baked lightmaps were authored with yet another sun the two can
|
||||||
|
// disagree, which the strength floor and the receivers' min-clamp keep
|
||||||
|
// plausible rather than doubled.
|
||||||
|
float sun[3] = {scene.shadow_rows[24], scene.shadow_rows[25],
|
||||||
|
scene.shadow_rows[26]};
|
||||||
|
const float slen =
|
||||||
|
std::sqrt(sun[0] * sun[0] + sun[1] * sun[1] + sun[2] * sun[2]);
|
||||||
|
if (slen < 0.5f) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (float& v : sun) {
|
||||||
|
v /= slen;
|
||||||
|
}
|
||||||
|
if (sun[1] < 0.08f) {
|
||||||
|
return; // sun at/below the horizon: no usable static sun term
|
||||||
|
}
|
||||||
|
const float zl[3] = {-sun[0], -sun[1], -sun[2]};
|
||||||
|
float xl[3] = {zl[2], 0.0f, -zl[0]};
|
||||||
|
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 radius = std::clamp(
|
||||||
|
float(REXCVAR_GET(skate3_native_render_scene_shadow_static_radius)),
|
||||||
|
20.0f, 500.0f);
|
||||||
|
// Depth window +-2r along the axis: covers cliffs/towers far above the
|
||||||
|
// box at ~16-bit millimeter precision.
|
||||||
|
const float depth_half = radius * 2.0f;
|
||||||
|
const float texel_m = 2.0f * radius / float(g_r.static_sun_size);
|
||||||
|
const float* cam = scene.cam_pos;
|
||||||
|
// Upsert this frame's visible statics into the persistent caster cache
|
||||||
|
// (same static filter as the world-shadow map pass). scene.items is the
|
||||||
|
// game's view-culled list; drawing the map from it directly popped
|
||||||
|
// shadows with the camera; the cache keeps once-seen geometry casting
|
||||||
|
// from any direction. A new or content-changed record marks the map
|
||||||
|
// dirty for a rebuild.
|
||||||
|
for (const DrawItem& item : scene.items) {
|
||||||
|
if (item.transparent || item.unlit || item.cloth_quads || item.water ||
|
||||||
|
item.water_ocean != 0 || item.skinned || item.ropa ||
|
||||||
|
item.char_family != 0 || item.dynobj != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Key: mesh + quantized world translation (placed instances of one
|
||||||
|
// mesh are distinct casters; a re-streamed identical placement maps
|
||||||
|
// back onto its record).
|
||||||
|
const auto q = [](float v) {
|
||||||
|
return uint64_t(int64_t(std::llround(double(v) * 8.0)));
|
||||||
|
};
|
||||||
|
uint64_t key = uint64_t(item.mesh) * 0x9E3779B97F4A7C15ull;
|
||||||
|
key ^= q(item.world[12]) * 0xC2B2AE3D27D4EB4Full;
|
||||||
|
key ^= (q(item.world[13]) + 0x165667B19E3779F9ull) * 0x27D4EB2F165667C5ull;
|
||||||
|
key ^= (q(item.world[14]) + 0x9E3779B97F4A7C15ull) * 0x85EBCA77C2B2AE63ull;
|
||||||
|
RendererState::StaticCaster& rec = g_r.static_casters[key];
|
||||||
|
if (rec.mesh == 0 || rec.fingerprint != item.fingerprint) {
|
||||||
|
rec.mesh = item.mesh;
|
||||||
|
rec.fingerprint = item.fingerprint;
|
||||||
|
std::memcpy(rec.world, item.world, sizeof(rec.world));
|
||||||
|
std::memcpy(rec.bbox_min, item.bbox_min, sizeof(rec.bbox_min));
|
||||||
|
std::memcpy(rec.bbox_max, item.bbox_max, sizeof(rec.bbox_max));
|
||||||
|
rec.diffuse_tex = item.diffuse_tex;
|
||||||
|
rec.clip = item.env_family == 7 || item.env_family == 9 ||
|
||||||
|
item.env_family == 10;
|
||||||
|
rec.draws.assign(item.draws.begin(), item.draws.end());
|
||||||
|
g_r.nsm_dirty = true;
|
||||||
|
}
|
||||||
|
rec.last_seen_frame = frame_number;
|
||||||
|
}
|
||||||
|
// Cache maintenance: distance eviction (region streamed away) plus a
|
||||||
|
// long staleness timeout, which bounds ghost shadows from content the
|
||||||
|
// game actually removed (deleted park-editor objects) at the cost of a
|
||||||
|
// late pop for casters never re-viewed.
|
||||||
|
if (frame_number >= g_r.static_casters_sweep_frame) {
|
||||||
|
g_r.static_casters_sweep_frame = frame_number + 600;
|
||||||
|
const float evict_r = radius * 6.0f;
|
||||||
|
for (auto it = g_r.static_casters.begin();
|
||||||
|
it != g_r.static_casters.end();) {
|
||||||
|
const RendererState::StaticCaster& rec = it->second;
|
||||||
|
const float ex = rec.world[12] - cam[0];
|
||||||
|
const float ey = rec.world[13] - cam[1];
|
||||||
|
const float ez = rec.world[14] - cam[2];
|
||||||
|
const bool far_away = ex * ex + ey * ey + ez * ez > evict_r * evict_r;
|
||||||
|
const bool stale = frame_number > rec.last_seen_frame + 30000;
|
||||||
|
if (far_away || stale) {
|
||||||
|
it = g_r.static_casters.erase(it);
|
||||||
|
g_r.nsm_dirty = true;
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cross-frame cache: the map re-renders only when something it depends
|
||||||
|
// on changed. Between rebuilds the stored rows and the map contents are
|
||||||
|
// served as-is; statics and the sun are near-constant, so this removes
|
||||||
|
// almost the entire steady-state caster cost (and freezes the raster,
|
||||||
|
// so foliage dapple cannot shimmer frame to frame).
|
||||||
|
// Dirty (content changed) rebuilds are RATE-LIMITED: dense areas stream
|
||||||
|
// new casters near-continuously, and an unthrottled dirty flag degraded
|
||||||
|
// to a full map redraw every frame. A <=30-frame shadow latency for
|
||||||
|
// newly streamed geometry is invisible; drift/sun rebuilds stay
|
||||||
|
// immediate.
|
||||||
|
bool rebuild = g_r.nsm_built_radius <= 0.0f ||
|
||||||
|
std::fabs(radius - g_r.nsm_built_radius) > 0.5f ||
|
||||||
|
frame_number >= g_r.nsm_rebuild_frame ||
|
||||||
|
(g_r.nsm_dirty &&
|
||||||
|
frame_number >= g_r.nsm_last_build_frame + 30);
|
||||||
|
if (!rebuild) {
|
||||||
|
// Camera drift from the built center, measured in the map plane: the
|
||||||
|
// INNER tile (radius/6) must keep covering the player's
|
||||||
|
// surroundings, so the threshold rides its extent.
|
||||||
|
const float dcx = cam[0] - g_r.nsm_center[0];
|
||||||
|
const float dcy = cam[1] - g_r.nsm_center[1];
|
||||||
|
const float dcz = cam[2] - g_r.nsm_center[2];
|
||||||
|
const float du = xl[0] * dcx + xl[1] * dcy + xl[2] * dcz;
|
||||||
|
const float dv = yl[0] * dcx + yl[1] * dcy + yl[2] * dcz;
|
||||||
|
rebuild = std::max(std::fabs(du), std::fabs(dv)) > radius * 0.0667f;
|
||||||
|
}
|
||||||
|
if (!rebuild) {
|
||||||
|
rebuild = sun[0] * g_r.nsm_sun[0] + sun[1] * g_r.nsm_sun[1] +
|
||||||
|
sun[2] * g_r.nsm_sun[2] <
|
||||||
|
0.999995f;
|
||||||
|
}
|
||||||
|
if (!rebuild) {
|
||||||
|
g_r.static_sun_valid = true; // serve the cached map + stored rows
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_r.nsm_dirty = false;
|
||||||
|
g_r.nsm_rebuild_frame = frame_number + 600;
|
||||||
|
g_r.nsm_last_build_frame = frame_number;
|
||||||
|
g_r.nsm_built_radius = radius;
|
||||||
|
std::memcpy(g_r.nsm_sun, sun, sizeof(g_r.nsm_sun));
|
||||||
|
float o[3] = {cam[0], cam[1], cam[2]};
|
||||||
|
const float cx = xl[0] * o[0] + xl[1] * o[1] + xl[2] * o[2];
|
||||||
|
const float cy = yl[0] * o[0] + yl[1] * o[1] + yl[2] * o[2];
|
||||||
|
const float dx = cx - std::round(cx / texel_m) * texel_m;
|
||||||
|
const float dy = cy - std::round(cy / texel_m) * texel_m;
|
||||||
|
for (int a = 0; a < 3; ++a) {
|
||||||
|
o[a] -= xl[a] * dx + yl[a] * dy;
|
||||||
|
}
|
||||||
|
std::memcpy(g_r.nsm_center, o, sizeof(g_r.nsm_center));
|
||||||
|
// world -> map rows, shared by this caster pass and the receivers
|
||||||
|
// (b1 nsm_x/y/z): uc = dot(row.xyz, wp) + row.w.
|
||||||
|
float rows[12];
|
||||||
|
for (int a = 0; a < 3; ++a) {
|
||||||
|
rows[0 + a] = xl[a] / radius;
|
||||||
|
rows[4 + a] = yl[a] / radius;
|
||||||
|
rows[8 + a] = zl[a] / (2.0f * depth_half);
|
||||||
|
}
|
||||||
|
rows[3] = -(rows[0] * o[0] + rows[1] * o[1] + rows[2] * o[2]);
|
||||||
|
rows[7] = -(rows[4] * o[0] + rows[5] * o[1] + rows[6] * o[2]);
|
||||||
|
rows[11] = 0.5f - (rows[8] * o[0] + rows[9] * o[1] + rows[10] * o[2]);
|
||||||
|
std::memcpy(g_r.nsm_rows, rows, sizeof(rows));
|
||||||
|
g_r.nsm_depth_range = 2.0f * depth_half;
|
||||||
|
g_r.nsm_radius = radius;
|
||||||
|
// View-proj columns from the rows (same construction as the CSM
|
||||||
|
// lightvp; clip.y lands in the sampler's v = -y/2 + 0.5 convention via
|
||||||
|
// the raster y-flip).
|
||||||
|
float vp[16] = {};
|
||||||
|
for (int r = 0; r < 3; ++r) {
|
||||||
|
vp[r * 4 + 0] = rows[0 + r];
|
||||||
|
vp[r * 4 + 1] = rows[4 + r];
|
||||||
|
vp[r * 4 + 2] = rows[8 + r];
|
||||||
|
}
|
||||||
|
vp[12] = rows[3];
|
||||||
|
vp[13] = rows[7];
|
||||||
|
vp[14] = rows[11];
|
||||||
|
vp[15] = 1.0f;
|
||||||
|
// Finer-tile view-projections: same origin, radius/2 and radius/6,
|
||||||
|
// exact rescales of the far transform's X/Y columns (the shader selects
|
||||||
|
// tiles by the same ratios; keep in sync with its
|
||||||
|
// kRatioInner/kRatioMid). Snapping used the FAR texel grid, whose
|
||||||
|
// multiples are also finer-texel multiples (integer 1:2:6 chain), so
|
||||||
|
// every tile stays swim-free.
|
||||||
|
constexpr float kRatioMid = 2.0f;
|
||||||
|
constexpr float kRatioInner = 6.0f;
|
||||||
|
float vp_mid[16];
|
||||||
|
float vp_inner[16];
|
||||||
|
std::memcpy(vp_mid, vp, sizeof(vp));
|
||||||
|
std::memcpy(vp_inner, vp, sizeof(vp));
|
||||||
|
for (int r = 0; r < 4; ++r) {
|
||||||
|
vp_mid[r * 4 + 0] *= kRatioMid;
|
||||||
|
vp_mid[r * 4 + 1] *= kRatioMid;
|
||||||
|
vp_inner[r * 4 + 0] *= kRatioInner;
|
||||||
|
vp_inner[r * 4 + 1] *= kRatioInner;
|
||||||
|
}
|
||||||
|
nrhi::Cmd* cmd = context.cmd;
|
||||||
|
if (g_r.static_sun_in_srv) {
|
||||||
|
cmd->Barrier(g_r.static_sun, nrhi::ResourceState::kPixelShaderResource,
|
||||||
|
nrhi::ResourceState::kRenderTarget);
|
||||||
|
cmd->FlushBarriers();
|
||||||
|
g_r.static_sun_in_srv = false;
|
||||||
|
}
|
||||||
|
const float clear[4] = {1.0f, 1.0f, 0.0f, 0.0f};
|
||||||
|
cmd->ClearRenderTarget(g_r.static_sun, clear);
|
||||||
|
cmd->SetRenderTargets(g_r.static_sun, nullptr);
|
||||||
|
cmd->SetBindingLayout(g_r.layout);
|
||||||
|
cmd->SetConstantBuffer(9, g_r.bone_ring, bone_region);
|
||||||
|
const float size = float(g_r.static_sun_size);
|
||||||
|
// Per tile (0 = inner r/6, 1 = mid r/2, 2 = far), two phases each:
|
||||||
|
// opaque statics, then the alpha-tested families (trees, alphatest
|
||||||
|
// fences/grates) with the clip PSO and their diffuse.
|
||||||
|
for (int tile = 0; tile < 3; ++tile) {
|
||||||
|
const float* tvp = tile == 0 ? vp_inner : (tile == 1 ? vp_mid : vp);
|
||||||
|
// Cull threshold in FAR uc units (finer tiles cover 1/ratio of the
|
||||||
|
// far extent).
|
||||||
|
const float cull = tile == 0 ? 1.05f / kRatioInner
|
||||||
|
: tile == 1 ? 1.05f / kRatioMid
|
||||||
|
: 1.05f;
|
||||||
|
cmd->SetViewport(
|
||||||
|
nrhi::Viewport{size * tile, 0.0f, size, size, 0.0f, 1.0f});
|
||||||
|
cmd->SetScissor(nrhi::Rect{int32_t(g_r.static_sun_size) * tile, 0,
|
||||||
|
int32_t(g_r.static_sun_size) * (tile + 1),
|
||||||
|
int32_t(g_r.static_sun_size)});
|
||||||
|
for (int phase = 0; phase < 2; ++phase) {
|
||||||
|
cmd->SetPipeline(phase == 0 ? g_r.pso_shadow_caster
|
||||||
|
: g_r.pso_shadow_caster_clip);
|
||||||
|
for (const auto& [key, rec] : g_r.static_casters) {
|
||||||
|
if (rec.clip != (phase == 1)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto mit = g_r.meshes.find(rec.mesh);
|
||||||
|
// Fingerprint mismatch = the arena address was reused for
|
||||||
|
// different content; drawing it would raster a foreign mesh into
|
||||||
|
// the map. The record refreshes the next time the item is
|
||||||
|
// actually seen.
|
||||||
|
if (mit == g_r.meshes.end() ||
|
||||||
|
mit->second.fingerprint != rec.fingerprint) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Cull by the mesh bbox footprint in (far) map space.
|
||||||
|
float umin = std::numeric_limits<float>::max(), umax = -umin;
|
||||||
|
float vmin = umin, vmax = -umin;
|
||||||
|
for (int corner = 0; corner < 8; ++corner) {
|
||||||
|
const float px = (corner & 1) ? rec.bbox_max[0] : rec.bbox_min[0];
|
||||||
|
const float py = (corner & 2) ? rec.bbox_max[1] : rec.bbox_min[1];
|
||||||
|
const float pz = (corner & 4) ? rec.bbox_max[2] : rec.bbox_min[2];
|
||||||
|
float w[3];
|
||||||
|
for (int a = 0; a < 3; ++a) {
|
||||||
|
w[a] = px * rec.world[0 + a] + py * rec.world[4 + a] +
|
||||||
|
pz * rec.world[8 + a] + rec.world[12 + a];
|
||||||
|
}
|
||||||
|
const float u = rows[0] * w[0] + rows[1] * w[1] + rows[2] * w[2] +
|
||||||
|
rows[3];
|
||||||
|
const float v = rows[4] * w[0] + rows[5] * w[1] + rows[6] * w[2] +
|
||||||
|
rows[7];
|
||||||
|
umin = std::min(umin, u);
|
||||||
|
umax = std::max(umax, u);
|
||||||
|
vmin = std::min(vmin, v);
|
||||||
|
vmax = std::max(vmax, v);
|
||||||
|
}
|
||||||
|
if (umax < -cull || umin > cull || vmax < -cull || vmin > cull) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float constants[52] = {};
|
||||||
|
std::memcpy(constants, rec.world, sizeof(rec.world));
|
||||||
|
float* mvp = constants + 16;
|
||||||
|
for (int r = 0; r < 4; ++r) {
|
||||||
|
for (int col = 0; col < 4; ++col) {
|
||||||
|
float sum = 0.0f;
|
||||||
|
for (int k = 0; k < 4; ++k) {
|
||||||
|
sum += rec.world[r * 4 + k] * tvp[k * 4 + col];
|
||||||
|
}
|
||||||
|
mvp[r * 4 + col] = sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd->SetRootConstants(0, 52, constants, 0);
|
||||||
|
cmd->SetBufferSrv(3, g_r.bone_ring, bone_region);
|
||||||
|
if (phase == 1) {
|
||||||
|
nrhi::TextureView* srv = LookupResolvedTexture(rec.diffuse_tex);
|
||||||
|
cmd->SetTexture(1, srv != nullptr ? srv : g_r.white.srv);
|
||||||
|
}
|
||||||
|
cmd->SetVertexBuffer(mit->second.vb_view.buffer,
|
||||||
|
mit->second.vb_view.offset,
|
||||||
|
mit->second.vb_view.size_bytes,
|
||||||
|
mit->second.vb_view.stride);
|
||||||
|
cmd->SetIndexBuffer(mit->second.ib_view.buffer,
|
||||||
|
mit->second.ib_view.offset,
|
||||||
|
mit->second.ib_view.size_bytes);
|
||||||
|
for (const DrawEntry& draw : rec.draws) {
|
||||||
|
if (draw.prim == 4) {
|
||||||
|
cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList);
|
||||||
|
} else if (draw.prim == 6) {
|
||||||
|
cmd->SetPrimitiveTopology(
|
||||||
|
nrhi::PrimitiveTopology::kTriangleStrip);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cmd->DrawIndexed(draw.index_count, draw.start_index,
|
||||||
|
draw.base_vertex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd->Barrier(g_r.static_sun, nrhi::ResourceState::kRenderTarget,
|
||||||
|
nrhi::ResourceState::kPixelShaderResource);
|
||||||
|
cmd->FlushBarriers();
|
||||||
|
g_r.static_sun_in_srv = true;
|
||||||
|
g_r.static_sun_valid = true;
|
||||||
|
}
|
||||||
|
|
||||||
bool RenderShadowAtlas(const NativeGuestOutputRenderContext& context,
|
bool RenderShadowAtlas(const NativeGuestOutputRenderContext& context,
|
||||||
const FrameScene& scene, uint32_t bone_region,
|
const FrameScene& scene, uint32_t bone_region,
|
||||||
int32_t debug_mode, uint32_t* out_draws) {
|
int32_t debug_mode, uint32_t* out_draws) {
|
||||||
|
|
@ -5477,8 +5897,14 @@ bool RenderShadowAtlas(const NativeGuestOutputRenderContext& context,
|
||||||
const DrawItem* item;
|
const DrawItem* item;
|
||||||
uint32_t bone_offset;
|
uint32_t bone_offset;
|
||||||
bool bones;
|
bool bones;
|
||||||
|
// Alpha-tested caster (dynamicobject.alphatest): draws with the clip
|
||||||
|
// PSO and the item's diffuse bound so the cutout silhouette casts,
|
||||||
|
// not the full card quad.
|
||||||
|
bool clip;
|
||||||
|
nrhi::TextureView* clip_srv;
|
||||||
};
|
};
|
||||||
std::vector<Caster> casters;
|
std::vector<Caster> casters;
|
||||||
|
bool any_clip = false;
|
||||||
for (const DrawItem& item : scene.items) {
|
for (const DrawItem& item : scene.items) {
|
||||||
if (item.transparent || item.unlit || item.cloth_quads) {
|
if (item.transparent || item.unlit || item.cloth_quads) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -5494,7 +5920,11 @@ bool RenderShadowAtlas(const NativeGuestOutputRenderContext& context,
|
||||||
// plaza glass towers (a "reflection-like" soft dark blob high on the
|
// plaza glass towers (a "reflection-like" soft dark blob high on the
|
||||||
// facade, geometrically impossible as a real shadow: sun at 47 deg,
|
// facade, geometrically impossible as a real shadow: sun at 47 deg,
|
||||||
// lamp 8 m tall, blob at 96 m; the emulated frame has no such
|
// lamp 8 m tall, blob at 96 m; the emulated frame has no such
|
||||||
// shadow).
|
// shadow). Moreover the cascade transforms are a stylized
|
||||||
|
// near-vertical projection serving a ~12 m height window; tall
|
||||||
|
// static geometry projected through them shadows its own plan-view
|
||||||
|
// footprint. Live static shade comes from the separate sun-aligned
|
||||||
|
// static shadow map instead (RenderStaticSunMap).
|
||||||
if (!skinned && item.dynobj == 0 && !item.ropa && item.char_family == 0) {
|
if (!skinned && item.dynobj == 0 && !item.ropa && item.char_family == 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -5522,7 +5952,12 @@ bool RenderShadowAtlas(const NativeGuestOutputRenderContext& context,
|
||||||
if (!g_r.meshes.contains(item.mesh)) {
|
if (!g_r.meshes.contains(item.mesh)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Caster c{&item, 0, false};
|
Caster c{&item, 0, false, false, nullptr};
|
||||||
|
if (item.dynobj == 2) {
|
||||||
|
c.clip = true;
|
||||||
|
c.clip_srv = LookupResolvedTexture(item.diffuse_tex);
|
||||||
|
any_clip = true;
|
||||||
|
}
|
||||||
if (skinned) {
|
if (skinned) {
|
||||||
const uint32_t bytes = uint32_t(item.bones.size() * sizeof(float));
|
const uint32_t bytes = uint32_t(item.bones.size() * sizeof(float));
|
||||||
const uint32_t offset = (g_r.bone_ring_offset + 255u) & ~255u;
|
const uint32_t offset = (g_r.bone_ring_offset + 255u) & ~255u;
|
||||||
|
|
@ -5579,47 +6014,62 @@ bool RenderShadowAtlas(const NativeGuestOutputRenderContext& context,
|
||||||
float(tile), 0.0f, 1.0f});
|
float(tile), 0.0f, 1.0f});
|
||||||
cmd->SetScissor(nrhi::Rect{int32_t(tile) * ci, 0,
|
cmd->SetScissor(nrhi::Rect{int32_t(tile) * ci, 0,
|
||||||
int32_t(tile) * (ci + 1), int32_t(tile)});
|
int32_t(tile) * (ci + 1), int32_t(tile)});
|
||||||
for (const Caster& c : casters) {
|
// Two phases per cascade: opaque casters, then the alpha-tested
|
||||||
auto mit = g_r.meshes.find(c.item->mesh);
|
// set with the clip PSO and per-item diffuse.
|
||||||
if (mit == g_r.meshes.end()) {
|
for (int phase = 0; phase < (any_clip ? 2 : 1); ++phase) {
|
||||||
continue; // undecodable this frame; casts once the workers land
|
cmd->SetPipeline(phase == 0 ? g_r.pso_shadow_caster
|
||||||
}
|
: g_r.pso_shadow_caster_clip);
|
||||||
// A stale fingerprint is fine here: cloth decodes ride one frame
|
for (const Caster& c : casters) {
|
||||||
// behind the sim by design (dyn decode jobs).
|
if (c.clip != (phase == 1)) {
|
||||||
float constants[52] = {};
|
|
||||||
std::memcpy(constants, c.item->world, sizeof(c.item->world));
|
|
||||||
float* mvp = constants + 16;
|
|
||||||
for (int r = 0; r < 4; ++r) {
|
|
||||||
for (int col = 0; col < 4; ++col) {
|
|
||||||
float sum = 0.0f;
|
|
||||||
for (int k = 0; k < 4; ++k) {
|
|
||||||
sum += c.item->world[r * 4 + k] * lightvp[k * 4 + col];
|
|
||||||
}
|
|
||||||
mvp[r * 4 + col] = sum;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
constants[33] = c.bones ? 1.0f : 0.0f; // tint.g = skinned branch
|
|
||||||
cmd->SetRootConstants(0, 52, constants, 0);
|
|
||||||
cmd->SetBufferSrv(3, g_r.bone_ring,
|
|
||||||
bone_region + (c.bones ? c.bone_offset : 0));
|
|
||||||
cmd->SetVertexBuffer(mit->second.vb_view.buffer,
|
|
||||||
mit->second.vb_view.offset,
|
|
||||||
mit->second.vb_view.size_bytes,
|
|
||||||
mit->second.vb_view.stride);
|
|
||||||
cmd->SetIndexBuffer(mit->second.ib_view.buffer,
|
|
||||||
mit->second.ib_view.offset,
|
|
||||||
mit->second.ib_view.size_bytes);
|
|
||||||
for (const DrawEntry& draw : c.item->draws) {
|
|
||||||
if (draw.prim == 4) {
|
|
||||||
cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList);
|
|
||||||
} else if (draw.prim == 6) {
|
|
||||||
cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleStrip);
|
|
||||||
} else {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
cmd->DrawIndexed(draw.index_count, draw.start_index,
|
auto mit = g_r.meshes.find(c.item->mesh);
|
||||||
draw.base_vertex);
|
if (mit == g_r.meshes.end()) {
|
||||||
++shadow_draws;
|
continue; // undecodable this frame; casts once the workers land
|
||||||
|
}
|
||||||
|
// A stale fingerprint is fine here: cloth decodes ride one frame
|
||||||
|
// behind the sim by design (dyn decode jobs).
|
||||||
|
float constants[52] = {};
|
||||||
|
std::memcpy(constants, c.item->world, sizeof(c.item->world));
|
||||||
|
float* mvp = constants + 16;
|
||||||
|
for (int r = 0; r < 4; ++r) {
|
||||||
|
for (int col = 0; col < 4; ++col) {
|
||||||
|
float sum = 0.0f;
|
||||||
|
for (int k = 0; k < 4; ++k) {
|
||||||
|
sum += c.item->world[r * 4 + k] * lightvp[k * 4 + col];
|
||||||
|
}
|
||||||
|
mvp[r * 4 + col] = sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
constants[33] = c.bones ? 1.0f : 0.0f; // tint.g = skinned branch
|
||||||
|
cmd->SetRootConstants(0, 52, constants, 0);
|
||||||
|
cmd->SetBufferSrv(3, g_r.bone_ring,
|
||||||
|
bone_region + (c.bones ? c.bone_offset : 0));
|
||||||
|
if (phase == 1) {
|
||||||
|
cmd->SetTexture(1, c.clip_srv != nullptr ? c.clip_srv
|
||||||
|
: g_r.white.srv);
|
||||||
|
}
|
||||||
|
cmd->SetVertexBuffer(mit->second.vb_view.buffer,
|
||||||
|
mit->second.vb_view.offset,
|
||||||
|
mit->second.vb_view.size_bytes,
|
||||||
|
mit->second.vb_view.stride);
|
||||||
|
cmd->SetIndexBuffer(mit->second.ib_view.buffer,
|
||||||
|
mit->second.ib_view.offset,
|
||||||
|
mit->second.ib_view.size_bytes);
|
||||||
|
for (const DrawEntry& draw : c.item->draws) {
|
||||||
|
if (draw.prim == 4) {
|
||||||
|
cmd->SetPrimitiveTopology(
|
||||||
|
nrhi::PrimitiveTopology::kTriangleList);
|
||||||
|
} else if (draw.prim == 6) {
|
||||||
|
cmd->SetPrimitiveTopology(
|
||||||
|
nrhi::PrimitiveTopology::kTriangleStrip);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cmd->DrawIndexed(draw.index_count, draw.start_index,
|
||||||
|
draw.base_vertex);
|
||||||
|
++shadow_draws;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6356,6 +6806,7 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
const int32_t debug_mode = REXCVAR_GET(skate3_native_render_scene_debug);
|
const int32_t debug_mode = REXCVAR_GET(skate3_native_render_scene_debug);
|
||||||
shadow_ready =
|
shadow_ready =
|
||||||
RenderShadowAtlas(context, scene, bone_region, debug_mode, &shadow_draws);
|
RenderShadowAtlas(context, scene, bone_region, debug_mode, &shadow_draws);
|
||||||
|
RenderStaticSunMap(context, scene, bone_region, debug_mode, frame_number);
|
||||||
g_pw_shadow.Add(perf_ns_since(shadow_t0));
|
g_pw_shadow.Add(perf_ns_since(shadow_t0));
|
||||||
// Scene-level flicker probe: the shadow cascade rows (or validity/readiness)
|
// Scene-level flicker probe: the shadow cascade rows (or validity/readiness)
|
||||||
// returning EXACTLY to the previous-but-one frame's state; hair samples the
|
// returning EXACTLY to the previous-but-one frame's state; hair samples the
|
||||||
|
|
@ -6482,6 +6933,53 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
cb[59] = 1.0f;
|
cb[59] = 1.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// PCSS soft-shadow rows (sh_pcss / sh_pcss2, cb[136..143]). Filled
|
||||||
|
// whenever the light rows are valid; the static sun-shadow sampler
|
||||||
|
// shares the angular knobs even on frames with no dynamic casters.
|
||||||
|
if (scene.shadow_valid) {
|
||||||
|
cb[136] =
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_shadow_pcss) ? 1.0f : 0.0f;
|
||||||
|
cb[137] = std::tan(
|
||||||
|
0.5f * 0.01745329f *
|
||||||
|
float(REXCVAR_GET(skate3_native_render_scene_shadow_pcss_sun_deg)));
|
||||||
|
cb[138] =
|
||||||
|
float(REXCVAR_GET(skate3_native_render_scene_shadow_pcss_max_m));
|
||||||
|
cb[139] = 0.0f; // atlas slope bias unused (dynamic casters only)
|
||||||
|
cb[140] =
|
||||||
|
float(REXCVAR_GET(skate3_native_render_scene_shadow_pcss_blocker_m));
|
||||||
|
const float zlen =
|
||||||
|
std::sqrt(sh[16] * sh[16] + sh[17] * sh[17] + sh[18] * sh[18]);
|
||||||
|
const float xlen =
|
||||||
|
std::sqrt(sh[0] * sh[0] + sh[1] * sh[1] + sh[2] * sh[2]);
|
||||||
|
cb[141] = zlen > 1e-8f ? 1.0f / zlen : 0.0f;
|
||||||
|
cb[142] = xlen;
|
||||||
|
cb[143] = float(
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_shadow_pcss_min_texel));
|
||||||
|
}
|
||||||
|
// Native static sun-shadow rows (nsm_x/y/z + params, cb[144..163]);
|
||||||
|
// nsm_p.x = 0 disables the term in every branch.
|
||||||
|
if (g_r.static_sun_valid) {
|
||||||
|
std::memcpy(cb + 144, g_r.nsm_rows, sizeof(g_r.nsm_rows));
|
||||||
|
cb[156] = std::clamp(
|
||||||
|
float(REXCVAR_GET(skate3_native_render_scene_shadow_static_strength)),
|
||||||
|
0.0f, 1.0f);
|
||||||
|
cb[157] = 1.0f / g_r.nsm_radius; // far-tile uc units per meter
|
||||||
|
cb[158] = g_r.nsm_depth_range; // meters per depth unit
|
||||||
|
// Filter floor: 2 physical texels (uc spans [-1,1] over one tile);
|
||||||
|
// the map has no dilation blur, so a sub-texel kernel exposes raw
|
||||||
|
// stair-step aliasing at full strength; two texels of
|
||||||
|
// rotated-poisson filtering dissolve the steps into a smooth edge.
|
||||||
|
cb[159] = 2.0f * 2.0f *
|
||||||
|
float(REXCVAR_GET(
|
||||||
|
skate3_native_render_scene_shadow_pcss_min_texel)) /
|
||||||
|
float(std::max(1u, g_r.static_sun_size));
|
||||||
|
const float bias_m = float(
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_shadow_static_bias));
|
||||||
|
cb[160] = bias_m / g_r.nsm_depth_range; // base receiver bias
|
||||||
|
cb[161] = bias_m / g_r.nsm_depth_range; // slope-scaled term
|
||||||
|
cb[162] = 0.0f; // reserved (cascade ratios are shader constants)
|
||||||
|
cb[163] = float(std::max(1u, g_r.static_sun_size)); // tile px
|
||||||
|
}
|
||||||
// Exact world-shading frame rows (valid whenever the env-family PS bank
|
// Exact world-shading frame rows (valid whenever the env-family PS bank
|
||||||
// was captured this frame, independent of the shadow ATLAS being
|
// was captured this frame, independent of the shadow ATLAS being
|
||||||
// rendered; draw_item only selects the exact branch when
|
// rendered; draw_item only selects the exact branch when
|
||||||
|
|
@ -6553,7 +7051,16 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
// (draw_item re-pairs with the item's real cube when one resolves).
|
// (draw_item re-pairs with the item's real cube when one resolves).
|
||||||
cmd->SetTexturePair(7, g_r.white_cube.srv,
|
cmd->SetTexturePair(7, g_r.white_cube.srv,
|
||||||
shadow_ready ? g_r.shadow_srv_final : g_r.white.srv);
|
shadow_ready ? g_r.shadow_srv_final : g_r.white.srv);
|
||||||
|
// v2 material table default (t8/t9 white) + the static sun-shadow map
|
||||||
|
// at its third entry (t10); every lit branch samples it, so the table
|
||||||
|
// must be bound for all draws, not only the v2/ocean re-binds.
|
||||||
|
nrhi::TextureView* t8_default[3] = {
|
||||||
|
g_r.white.srv, g_r.white.srv,
|
||||||
|
g_r.static_sun_valid ? g_r.static_sun_srv : g_r.white.srv};
|
||||||
|
cmd->SetTextures(8, t8_default, 3);
|
||||||
}
|
}
|
||||||
|
nrhi::TextureView* const nsm_view =
|
||||||
|
g_r.static_sun_valid ? g_r.static_sun_srv : g_r.white.srv;
|
||||||
|
|
||||||
uint32_t drawn = 0;
|
uint32_t drawn = 0;
|
||||||
uint32_t item_index = 0;
|
uint32_t item_index = 0;
|
||||||
|
|
@ -7823,8 +8330,12 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
// World-shading v2 material pair (t8 detail + t9 decal spec); the exact
|
// World-shading v2 material pair (t8 detail + t9 decal spec); the exact
|
||||||
// ocean rides the same pair (t8 = second PCA component, t9 = overlay).
|
// ocean rides the same pair (t8 = second PCA component, t9 = overlay).
|
||||||
if (v2_flags != 0 || ocean_n2 || ocean_ov) {
|
if (v2_flags != 0 || ocean_n2 || ocean_ov) {
|
||||||
cmd->SetTexturePair(8, (v2_detail != nullptr ? v2_detail : &g_r.white)->srv,
|
// Three-entry bind: re-pairing only t8/t9 would reset the t10 static
|
||||||
(v2_spec2 != nullptr ? v2_spec2 : &g_r.white)->srv);
|
// sun-shadow map to the backend fallback.
|
||||||
|
nrhi::TextureView* t8_views[3] = {
|
||||||
|
(v2_detail != nullptr ? v2_detail : &g_r.white)->srv,
|
||||||
|
(v2_spec2 != nullptr ? v2_spec2 : &g_r.white)->srv, nsm_view};
|
||||||
|
cmd->SetTextures(8, t8_views, 3);
|
||||||
}
|
}
|
||||||
// ROPA shape blend (see RendererState::ropa_shapes): combine the shape
|
// ROPA shape blend (see RendererState::ropa_shapes): combine the shape
|
||||||
// generations with the kernel weights InterpolateDynamicItems computed
|
// generations with the kernel weights InterpolateDynamicItems computed
|
||||||
|
|
|
||||||
|
|
@ -766,6 +766,10 @@ struct RendererState {
|
||||||
// 3-tap cascade 1, format-convert-only cascade 2) into the atlas the
|
// 3-tap cascade 1, format-convert-only cascade 2) into the atlas the
|
||||||
// scene pass samples at t5.
|
// scene pass samples at t5.
|
||||||
nrhi::Pipeline* pso_shadow_caster = nullptr;
|
nrhi::Pipeline* pso_shadow_caster = nullptr;
|
||||||
|
// Alpha-tested caster variant (ps_shadow_caster_clip): binds the item's
|
||||||
|
// diffuse at t0 and clips at the world families' ALPHAREF so foliage
|
||||||
|
// cards and alphatest fences cast their cutout silhouette.
|
||||||
|
nrhi::Pipeline* pso_shadow_caster_clip = nullptr;
|
||||||
nrhi::Pipeline* pso_shadow_blur = nullptr;
|
nrhi::Pipeline* pso_shadow_blur = nullptr;
|
||||||
nrhi::Texture* shadow_raw = nullptr; // caster pass target
|
nrhi::Texture* shadow_raw = nullptr; // caster pass target
|
||||||
nrhi::Texture* shadow_mid = nullptr; // hblur output
|
nrhi::Texture* shadow_mid = nullptr; // hblur output
|
||||||
|
|
@ -794,11 +798,59 @@ struct RendererState {
|
||||||
// first 256 bytes are the original 16-row block, rows 16-18 carry the
|
// first 256 bytes are the original 16-row block, rows 16-18 carry the
|
||||||
// dynamicobject world-shadow transform (dyn_ws*), rows 19-22 the
|
// dynamicobject world-shadow transform (dyn_ws*), rows 19-22 the
|
||||||
// flowingwateralpha m_params (wat_p*), rows 23-33 the ocean PCA/material
|
// flowingwateralpha m_params (wat_p*), rows 23-33 the ocean PCA/material
|
||||||
// rows and the oceanreflection fade row (oc_* / orf).
|
// rows and the oceanreflection fade row (oc_* / orf), rows 34-35 the
|
||||||
|
// PCSS soft-shadow parameters (sh_pcss / sh_pcss2).
|
||||||
static constexpr uint32_t kShadowCbRegions = 8;
|
static constexpr uint32_t kShadowCbRegions = 8;
|
||||||
static constexpr uint32_t kShadowCbSlice = 768;
|
static constexpr uint32_t kShadowCbSlice = 768;
|
||||||
nrhi::Buffer* shadow_cb = nullptr;
|
nrhi::Buffer* shadow_cb = nullptr;
|
||||||
uint8_t* shadow_cb_cpu = nullptr;
|
uint8_t* shadow_cb_cpu = nullptr;
|
||||||
|
// Native static sun-shadow map: a single camera-centered ortho depth map
|
||||||
|
// of the STATIC world along the material sun (RenderStaticSunMap),
|
||||||
|
// sampled at t10 by every lit branch (SampleStaticSun). RG16 to reuse
|
||||||
|
// the caster pipeline/clear conventions; G unused. nsm_rows are this
|
||||||
|
// frame's world->map transform rows for the b1 fill.
|
||||||
|
nrhi::Texture* static_sun = nullptr;
|
||||||
|
nrhi::TextureView* static_sun_srv = nullptr;
|
||||||
|
bool static_sun_in_srv = false;
|
||||||
|
bool static_sun_valid = false;
|
||||||
|
uint32_t static_sun_size = 0;
|
||||||
|
float nsm_rows[12] = {};
|
||||||
|
float nsm_depth_range = 1.0f; // meters per depth-map unit
|
||||||
|
float nsm_radius = 1.0f; // far-tile ortho half-extent in meters
|
||||||
|
// Cross-frame map cache: the map only re-renders when the camera drifts
|
||||||
|
// from the built center, the sun moves, the caster cache changed, or on
|
||||||
|
// a periodic safety rebuild (newly decoded meshes); statics and the sun
|
||||||
|
// are near-constant, so per-frame redraws of the full two-tile atlas
|
||||||
|
// were almost pure waste (and re-snapping the origin every frame made
|
||||||
|
// the foliage dapple shimmer).
|
||||||
|
float nsm_center[3] = {};
|
||||||
|
float nsm_sun[3] = {};
|
||||||
|
float nsm_built_radius = 0.0f;
|
||||||
|
bool nsm_dirty = true;
|
||||||
|
uint64_t nsm_rebuild_frame = 0;
|
||||||
|
uint64_t nsm_last_build_frame = 0;
|
||||||
|
// Persistent static-caster cache for the sun map. scene.items is the
|
||||||
|
// game's VIEW-CULLED draw list; rendering the map from it directly made
|
||||||
|
// shadows pop with the camera (look away and the caster leaves the list;
|
||||||
|
// geometry behind the camera never cast at all). Items upsert into this
|
||||||
|
// cache when visible and keep casting from it afterwards, validated
|
||||||
|
// against the mesh store's fingerprint at draw time. Eviction: distance
|
||||||
|
// from the camera (region streamed away) plus a long staleness timeout
|
||||||
|
// (bounds ghosts from content the game actually removed, e.g. deleted
|
||||||
|
// park-editor objects).
|
||||||
|
struct StaticCaster {
|
||||||
|
uint32_t mesh = 0;
|
||||||
|
uint64_t fingerprint = 0;
|
||||||
|
float world[16] = {};
|
||||||
|
float bbox_min[3] = {};
|
||||||
|
float bbox_max[3] = {};
|
||||||
|
uint32_t diffuse_tex = 0;
|
||||||
|
bool clip = false;
|
||||||
|
std::vector<DrawEntry> draws;
|
||||||
|
uint64_t last_seen_frame = 0;
|
||||||
|
};
|
||||||
|
std::unordered_map<uint64_t, StaticCaster> static_casters;
|
||||||
|
uint64_t static_casters_sweep_frame = 0;
|
||||||
// dynamicobject static world-shadow map (512x512, same convention as the
|
// dynamicobject static world-shadow map (512x512, same convention as the
|
||||||
// game's own: x = light-space depth, sge(x >= ray) = lit): re-rendered
|
// game's own: x = light-space depth, sge(x >= ray) = lit): re-rendered
|
||||||
// natively from the frame's STATIC world items with the captured c5/c6/c7
|
// natively from the frame's STATIC world items with the captured c5/c6/c7
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue