render: dynamicobject v2 - per-pixel maps + native static world-shadow

This commit is contained in:
mchughalex 2026-07-17 10:46:05 +01:00
parent a64c9afb9a
commit 70b4e691bc
4 changed files with 2530 additions and 1897 deletions

View file

@ -62,6 +62,14 @@ cbuffer S : register(b1) {
// cross(binormal, normal) x handedness to the game's tangent; +-1, // cross(binormal, normal) x handedness to the game's tangent; +-1,
// cvar-tunable against the emulated reference), yzw spare. // cvar-tunable against the emulated reference), yzw spare.
float4 sh_v2; float4 sh_v2;
// dynamicobject static world-shadow transform (the game's PS c5/c6/c7):
// u = dot(wp4, dyn_wsx) * 0.5 + 0.5, v = dot(wp4, dyn_wsy) * -0.5 + 0.5,
// ray depth = dot(wp4, dyn_wsz). Compared against the natively
// re-rendered 512x512 baked-shade map at t4 (misc.z flag 8; see the
// dynobj branch).
float4 dyn_wsx;
float4 dyn_wsy;
float4 dyn_wsz;
}; };
// 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
@ -88,10 +96,10 @@ Texture2D<float4> decal_art : register(t4);
Texture2D<float4> normal_map : register(t5); Texture2D<float4> normal_map : register(t5);
Texture2D<float2> shadow_atlas : register(t7); Texture2D<float2> shadow_atlas : register(t7);
TextureCube<float4> env_cube : register(t6); TextureCube<float4> env_cube : register(t6);
// World-shading v2 material maps (the t8/t9 pair table): the environment // World-shading v2 material maps (the t8/t9 pair table): the detail normal
// families' detail normal map (t8, sampled at uv * misc.w) and the decal // map (t8, sampled at uv * misc.w) and the spec/ecc masks (t9). Sampled
// families' spec/ecc masks (t9). Sampled only when the misc.z bind flags // only when the misc.z bind flags say so (env fams 1-4 and the
// say so (fams 1-4; see the env branch). // 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);
// 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],
@ -231,6 +239,37 @@ 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));
} }
// Screen-space UV-gradient tangent frame (cotangent reconstruction from the
// position/uv derivatives; signs/lengths calibrated against the meshes' REAL
// stored TBN; see the env-family branch notes). The RAW screen frame is
// exact for the GetTangentLight kd SIGN terms under ANY UV orientation:
// bowl/ramp walls map their texture rotated relative to world-up, and
// mirrored islands flip dP/dU exactly like the stored tangent does.
void ScreenTangentFrame(float3 wn, float3 rpos, float2 uv,
out float3 tt, out float3 bb) {
float3 dp1 = ddx(rpos), dp2 = ddy(rpos);
float2 du1 = ddx(uv), du2 = ddy(uv);
float3 dp2p = cross(dp2, wn), dp1p = cross(wn, dp1);
tt = dp2p * du1.x + dp1p * du2.x;
bb = dp2p * du1.y + dp1p * du2.y;
tt *= -rsqrt(max(dot(tt, tt), 1e-12));
bb *= rsqrt(max(dot(bb, bb), 1e-12));
}
// kd/normal-mapping axes: prefer the mesh's STORED frame (binormal +
// handedness in tanb, T = cross(B, N) x handedness x calibration polarity)
// - authoring decides per UV island whether the frame follows a mirror,
// which no derivative frame can know (per-island brightness steps on the
// wooden ramp panels / faint bowl striping). The screen frame is the
// fallback for meshes without one.
void KdAxes(float4 tanb, float3 wn, float3 tt_s, float3 bb_s,
out float3 kt, out float3 kb) {
kt = tt_s;
kb = bb_s;
if (tanb.w > 0.1) {
kb = normalize(tanb.xyz);
kt = cross(kb, wn) * ((tanb.w > 0.6 ? 1.0 : -1.0) * sh_v2.x);
}
}
float4 ps_main(VSOut i) : SV_Target { float4 ps_main(VSOut i) : SV_Target {
if (tint.a > 0.0) { if (tint.a > 0.0) {
return tint; return tint;
@ -284,13 +323,17 @@ float4 ps_main(VSOut i) : SV_Target {
} }
// dynamicobject.fx props (cam_pos.w = -(20 + variant): -21 default, // dynamicobject.fx props (cam_pos.w = -(20 + variant): -21 default,
// -22 alphatest). Rigid movable objects (dispensers, dumpsters, benches, // -22 alphatest). Rigid movable objects (dispensers, dumpsters, benches,
// cans). Lit with the game's own dynamicobject model (verified exact): // cans). Lit with the game's own dynamicobject model (verified exact
// key sun light + bounce + flat ambient, gated by // against an offline ucode evaluation): key sun light + bounce +
// the CSM shadow (the static world-shadow map is approximated as fully lit // flat ambient, gated by min(CSM, max(static world shade, c8.w floor)),
// - its floor c8.w bounds it), then fog -> exposure -> tonemap -> sqrt and // then fog -> exposure -> tonemap -> sqrt and the postfx uber 1.41.
// the postfx uber 1.41. v1 uses the geometric normal (cross of the mesh // v2: misc.z
// tangent/binormal) with the flat normal-map kd 0.93429; the base/detail/ // carries the material bind flags (1 = base normal at t5, 2 = detail at
// spec normal maps are v2. // t8 sampled at uv * misc.w, 4 = spec/ecc masks at t9: the env fams 1-4
// encoding); the mapped world normal drives key/bounce, the real
// GetTangentLight kd replaces the flat fold, and the phong spec returns.
// With no maps bound the flat-map fold 0.39 * 2.39562 = 0.93429 and the
// vertex normal apply (the v1 look).
if (cam_pos.w < -20.5) { if (cam_pos.w < -20.5) {
if (cam_pos.w < -21.5) { if (cam_pos.w < -21.5) {
clip(albedo.a - 0.1176); // dynamicobject.alphatest: ALPHAREF 30 clip(albedo.a - 0.1176); // dynamicobject.alphatest: ALPHAREF 30
@ -299,19 +342,107 @@ float4 ps_main(VSOut i) : SV_Target {
float3 n = dot(i.nrm, i.nrm) > 0.01 float3 n = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm) ? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos))); : normalize(cross(ddx(i.rpos), ddy(i.rpos)));
float ndl = dot(n, dyn_sun.xyz); uint v2f = (uint)(misc.z + 0.5);
// Tangent axes for the kd sign dots / normal mapping / tangent-space
// spec mirror: the stored per-vertex frame when the mesh carries one
// (packed by DecodeMesh from the usage-6/7 tangent+binormal pair),
// screen-space UV-gradient frame otherwise.
float3 kt = float3(1.0, 0.0, 0.0), kb = float3(0.0, 1.0, 0.0);
if (v2f != 0u) {
float3 tt_s, bb_s;
ScreenTangentFrame(n, i.rpos, i.uv, tt_s, bb_s);
KdAxes(i.tanb, n, tt_s, bb_s, kt, kb);
}
// GetTangentLight's fixed SIGNED tangent-space light: the signs come
// from the frame axes dotted with the dynobj bank's own sun (c9:
// dyn_sun, not the world families' sh_sun).
float3 Lt = float3(0.58 * sign(dot(kt, dyn_sun.xyz)),
0.62 * sign(dot(kb, dyn_sun.xyz)), 0.39);
// vnd = (2*base.xy + 2*detail.xy - 2, 2*base.z - 1), NORMALIZED for
// both the kd dot and the TBN mapping (both dynobj variants).
float3 vndn = float3(0.0, 0.0, 1.0);
float3 wn = n;
float kd = 0.93429;
if ((v2f & 1u) != 0u) {
float3 nmv = normal_map.Sample(smp, i.uv).rgb;
float2 dxy = (v2f & 2u) != 0u
? detail_map.Sample(smp, i.uv * misc.w).rg
: float2(0.5, 0.5);
vndn = normalize(
float3(nmv.xy * 2.0 + dxy * 2.0 - 2.0, nmv.z * 2.0 - 1.0));
wn = normalize(vndn.x * kt + vndn.y * kb + vndn.z * n);
kd = dot(vndn, Lt) * 2.39562;
}
float ndl = dot(wn, dyn_sun.xyz);
float key = saturate(ndl); // key light gated on N.L >= 0 float key = saturate(ndl); // key light gated on N.L >= 0
float bounce = saturate(dot(n, float3(-dyn_sun.x, dyn_sun.y, -dyn_sun.z))); float bounce = saturate(dot(wn, float3(-dyn_sun.x, dyn_sun.y, -dyn_sun.z)));
// CSM shadow (shared receiver rows at t5); the static world-shadow map is // CSM shadow (shared receiver rows at t7). extra_bias -1 = the game's
// not rendered natively, so its term is 1 and the min collapses to the // own per-cascade dynamicobject receive bias (props are casters;
// CSM (bounded below by the c8.w floor, dyn_misc.y). extra_bias -1 = // without it their flat tops self-shadow and flicker).
// the game's own per-cascade dynamicobject receive bias (props are
// casters; without it their flat tops self-shadow and flicker).
float s = SampleCsmShadow(i.rpos + cam_pos.xyz, -1.0); float s = SampleCsmShadow(i.rpos + cam_pos.xyz, -1.0);
float shadow = min(s * (ndl >= 0.0 ? 1.0 : 0.0), max(1.0, dyn_misc.y)); // Static world-shadow term: the game's baked building/tree shade,
// 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:
// tap.x >= ray depth reads lit; uncovered texels hold the far clear).
// Without the map the term is 1 and the min collapses to the CSM:
// props standing in baked shade lit at full key (the newspaper-machine
// mint brightening).
float world = 1.0;
if ((v2f & 8u) != 0u) {
float4 wp4 = float4(i.rpos + cam_pos.xyz, 1.0);
float2 wuv = float2(dot(wp4, dyn_wsx) * 0.5 + 0.5,
dot(wp4, dyn_wsy) * -0.5 + 0.5);
float rdw = dot(wp4, dyn_wsz);
float2 st = wuv * 512.0 - 0.5;
float2 fw = frac(st);
int2 b0 = int2(floor(st));
float4 t;
t.x = decal_art.Load(int3(clamp(b0, int2(0, 0), int2(511, 511)), 0)).x >=
rdw
? 1.0
: 0.0;
t.y = decal_art.Load(int3(clamp(b0 + int2(1, 0), int2(0, 0),
int2(511, 511)),
0)).x >= rdw
? 1.0
: 0.0;
t.z = decal_art.Load(int3(clamp(b0 + int2(0, 1), int2(0, 0),
int2(511, 511)),
0)).x >= rdw
? 1.0
: 0.0;
t.w = decal_art.Load(int3(clamp(b0 + int2(1, 1), int2(0, 0),
int2(511, 511)),
0)).x >= rdw
? 1.0
: 0.0;
world = lerp(lerp(t.x, t.y, fw.x), lerp(t.z, t.w, fw.x), fw.y);
}
float shadow = min(s * (ndl >= 0.0 ? 1.0 : 0.0), max(world, dyn_misc.y));
float3 lighting = key * shadow + bounce * dyn_amb.w + dyn_amb.rgb; float3 lighting = key * shadow + bounce * dyn_amb.w + dyn_amb.rgb;
// GetTangentLight with the neutral (flat) normal map: 0.39 * 2.39562. float3 lin = lighting * kd * dlin;
float3 lin = lighting * 0.93429 * dlin; // Phong spec vs the material's spec mask (t9.x) / eccentricity (t9.y),
// x shadow, tint (2.1, 1.8, 1.5). Two variants: the default PS
// reflects the fixed SIGNED tangent-space light about vnd with the eye
// transformed into tangent space (it shares the kd sign dots); the
// alphatest PS reflects the fixed WORLD light (-0.14, 0.5, 0.9) about
// wN exactly like baseenvironment.
if ((v2f & 4u) != 0u) {
float2 m2 = spec2_map.Sample(smp, i.uv).rg;
float3 E = -normalize(i.rpos);
float bp;
if (cam_pos.w < -21.5) {
float3 Ls = float3(-0.14, 0.5, 0.9);
float3 refl = Ls - 2.0 * wn * dot(wn, Ls);
bp = saturate(dot(E, -refl));
} else {
float3 Et = normalize(float3(dot(E, kt), dot(E, kb), dot(E, n)));
float3 refl = Lt - 2.0 * dot(vndn, Lt) * vndn;
bp = saturate(dot(Et, -refl));
}
float ks = bp > 0.0 ? pow(max(bp, 1e-6), 10.0 + 290.0 * m2.y) : 0.0;
lin += ks * float3(2.1, 1.8, 1.5) * (shadow * m2.x);
}
// Fog -> exposure -> tonemap -> sqrt, then the 1.41 uber scene multiplier. // Fog -> exposure -> tonemap -> sqrt, then the 1.41 uber scene multiplier.
float fdist = length(i.rpos); float fdist = length(i.rpos);
float f1 = saturate(fdist * sh_fogp.x + sh_fogp.y); float f1 = saturate(fdist * sh_fogp.x + sh_fogp.y);
@ -611,10 +742,9 @@ float4 ps_main(VSOut i) : SV_Target {
} }
// Tangent frames shared by kd and the reflective normal mapping. // Tangent frames shared by kd and the reflective normal mapping.
// Two variants of the same construction: // Two variants of the same construction:
// - tt_s/bb_s = the RAW screen-space UV-gradient frame. Exact for // - tt_s/bb_s = the RAW screen-space UV-gradient frame
// the kd SIGN terms under ANY UV orientation; bowl/ramp walls // (ScreenTangentFrame: exact for the kd sign terms under any UV
// map their texture rotated relative to world-up, and mirrored // orientation).
// islands flip dP/dU exactly like the stored tangent does.
// - tt/bb = analytic world-up axes carrying the screen frame's // - tt/bb = analytic world-up axes carrying the screen frame's
// signs; degree-accurate where the mapping IS world-up aligned // signs; degree-accurate where the mapping IS world-up aligned
// (building facades, calibrated for the glass reflections). // (building facades, calibrated for the glass reflections).
@ -626,13 +756,7 @@ float4 ps_main(VSOut i) : SV_Target {
float3 tt = float3(1.0, 0.0, 0.0), bb = float3(0.0, 1.0, 0.0); float3 tt = float3(1.0, 0.0, 0.0), bb = float3(0.0, 1.0, 0.0);
float3 tt_s = tt, bb_s = bb; float3 tt_s = tt, bb_s = bb;
if (have_vnd || (overlay.w > 3.5 && abs(misc.z - 3.0) > 0.5)) { if (have_vnd || (overlay.w > 3.5 && abs(misc.z - 3.0) > 0.5)) {
float3 dp1 = ddx(i.rpos), dp2 = ddy(i.rpos); ScreenTangentFrame(wn, i.rpos, i.uv, tt, bb);
float2 du1 = ddx(i.uv), du2 = ddy(i.uv);
float3 dp2p = cross(dp2, wn), dp1p = cross(wn, dp1);
tt = dp2p * du1.x + dp1p * du2.x;
bb = dp2p * du1.y + dp1p * du2.y;
tt *= -rsqrt(max(dot(tt, tt), 1e-12));
bb *= rsqrt(max(dot(bb, bb), 1e-12));
tt_s = tt; tt_s = tt;
bb_s = bb; bb_s = bb;
float3 bb2 = float3(0.0, 1.0, 0.0) - wn * wn.y; float3 bb2 = float3(0.0, 1.0, 0.0) - wn * wn.y;
@ -644,18 +768,9 @@ float4 ps_main(VSOut i) : SV_Target {
bb = bb2 * (dot(bb2, bb) >= 0.0 ? 1.0 : -1.0); bb = bb2 * (dot(bb2, bb) >= 0.0 ? 1.0 : -1.0);
} }
} }
// kd axes: prefer the mesh's STORED frame (binormal + handedness in // kd axes: stored per-vertex frame with the screen fallback (KdAxes).
// i.tanb, T = cross(B, N) x handedness x calibration polarity); float3 kt, kb;
// authoring decides per UV island whether the frame follows a KdAxes(i.tanb, wn, tt_s, bb_s, kt, kb);
// mirror, which no derivative frame can know (per-island brightness
// steps on the wooden ramp panels / faint bowl striping). Screen
// frame is the fallback for meshes without one.
float3 kt = tt_s, kb = bb_s;
if (i.tanb.w > 0.1) {
kb = normalize(i.tanb.xyz);
kt = cross(kb, wn) *
((i.tanb.w > 0.6 ? 1.0 : -1.0) * sh_v2.x);
}
if (have_vnd) { if (have_vnd) {
// The mapped world normal feeds the spec mirror below; matte spec // The mapped world normal feeds the spec mirror below; matte spec
// is broad-lobed, so exact axis precision matters less than the // is broad-lobed, so exact axis precision matters less than the

File diff suppressed because it is too large Load diff

View file

@ -95,6 +95,13 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_world_v2, true, "Skate 3",
"terms; v1 folded them to the flat-map constants). Off = the v1 " "terms; v1 folded them to the flat-map constants). Off = the v1 "
"flat response, for A/B comparison.") "flat response, for A/B comparison.")
.lifecycle(rex::cvar::Lifecycle::kHotReload); .lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_dynobj_v2, true, "Skate 3",
"Dynamicobject shading v2: per-pixel normal/detail/spec maps on the "
"exact dynamicobject families (mapped world normal, the game's "
"GetTangentLight kd and both phong spec variants; v1 folded them "
"to the flat-map constants). Off = the v1 flat response, for A/B "
"comparison.")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_ssao, true, "Skate 3", REXCVAR_DEFINE_BOOL(skate3_native_render_scene_ssao, true, "Skate 3",
"Screen-space ambient occlusion (GTAO) in the native renderer: soft " "Screen-space ambient occlusion (GTAO) in the native renderer: soft "
"contact shading where surfaces meet (under ledges, rails, vehicles, " "contact shading where surfaces meet (under ledges, rails, vehicles, "
@ -1083,6 +1090,10 @@ bool g_proxy_frame_done = false;
float g_dynobj_rows[10] = {}; float g_dynobj_rows[10] = {};
bool g_dynobj_have = false; bool g_dynobj_have = false;
bool g_dynobj_frame_done = false; bool g_dynobj_frame_done = false;
// dynamicobject static world-shadow transform rows (PS c5/c6/c7), captured
// alongside g_dynobj_rows, see FrameScene::dynobj_ws.
float g_dynobj_ws[12] = {};
bool g_dynobj_ws_have = false;
// Dynamic entities (characters, movable props) live entirely in transient // Dynamic entities (characters, movable props) live entirely in transient
// per-frame arenas: the context, its record, mesh pointers, transforms and // per-frame arenas: the context, its record, mesh pointers, transforms and
// bone palettes are all recycled before frame end. The complete DrawItem is // bone palettes are all recycled before frame end. The complete DrawItem is
@ -4378,28 +4389,66 @@ bool CaptureSkinnedState(uint8_t* base, uint32_t bank, uint32_t palette_base,
return false; return false;
} }
} else { } else {
// Dropped-garment gate: a garment the game removed (numCloth back to
// 0, VB objects freed) must never publish rigid; whatever this bank
// holds is foreign (the game draws the ctx skinned now), and a rigid
// publish would pair a live world with our retained stale drape.
if (skate3::native_entity::RopaGarmentDropped(base, item.ctx,
item.vb_obj)) {
g_ropa_stale.fetch_add(1, std::memory_order_relaxed);
return false;
}
// The garment's rigid world is the accepted draw-time bank matrix // The garment's rigid world is the accepted draw-time bank matrix
// (c188/c191): the game stages it tick-exact with the deformed VB // (c188/c191): the game stages it tick-exact with the deformed VB
// content and with the body palettes packed at EndJobs. (A guest-side // content and with the body palettes packed at EndJobs. (A guest-side
// entity L2W read at StartJobs predates the tick's locomotion update; // entity L2W read at StartJobs predates the tick's locomotion update;
// serving that as the draw world rendered the whole shirt one guest // serving that as the draw world rendered the whole shirt one guest
// tick behind the body, a constant velocity-proportional drape lag.) // tick behind the body, a constant velocity-proportional drape lag.)
const uint32_t m = main_pass ? 191u : 188u; uint32_t m = main_pass ? 191u : 188u;
float rows[12]; float rows[12];
bool plausible = true; const auto read_rows = [&](uint32_t reg) -> bool {
for (int r = 0; r < 3 && plausible; ++r) { bool ok = true;
float n = 0.0f; for (int r = 0; r < 3 && ok; ++r) {
for (int i = 0; i < 4; ++i) { float n = 0.0f;
const float f = LoadGuestF32(base, bank + ((m + r) * 4 + i) * 4); for (int i = 0; i < 4; ++i) {
if (!(f > -1e7f && f < 1e7f)) { const float f = LoadGuestF32(base, bank + ((reg + r) * 4 + i) * 4);
plausible = false; if (!(f > -1e7f && f < 1e7f)) {
break; ok = false;
break;
}
rows[r * 4 + i] = f;
if (i < 3) n += f * f;
} }
rows[r * 4 + i] = f; ok = ok && n > 0.0025f && n < 400.0f && rows[r * 4 + 3] > -20000.f &&
if (i < 3) n += f * f; rows[r * 4 + 3] < 20000.f;
} }
plausible = plausible && n > 0.0025f && n < 400.0f && return ok;
rows[r * 4 + 3] > -20000.f && rows[r * 4 + 3] < 20000.f; };
const auto tail_row_at = [&](uint32_t reg) -> bool {
const float x = LoadGuestF32(base, bank + (reg * 4 + 0) * 4);
const float y = LoadGuestF32(base, bank + (reg * 4 + 1) * 4);
const float z = LoadGuestF32(base, bank + (reg * 4 + 2) * 4);
const float w = LoadGuestF32(base, bank + (reg * 4 + 3) * 4);
return std::fabs(x) <= 1e-4f && std::fabs(y) <= 1e-4f &&
std::fabs(z) <= 1e-4f && std::fabs(w - 1.0f) <= 1e-4f;
};
// The passes stage the matrix as a 4-row block each: main at
// c191..c194, shadow at c188..c191 (3 affine rows + a (0,0,0,1)
// homogeneous tail). The tail at m+3 is REQUIRED: it is the
// structural proof a matrix block lives at m. Without it the read is
// mid-palette; an 84-bone body bank's palette (c5/c8 onward) covers
// c188..c194 with bone matrices, and a bone matrix (world *
// inverse-bind) is a plausible, high-scoring affine that renders the
// garment rotated by the bind rotation and offset meters from the
// body while tracking the skater.
bool plausible = read_rows(m) && tail_row_at(m + 3);
// The shadow block's tail lands exactly on c191 = the main block's
// first row. A main-pass read that finds that tail at c191 is seeing
// the post-shadow bank state, where c188..c190 still hold the same
// tick's matrix: consume that copy instead of refusing the capture.
if (!plausible && main_pass && tail_row_at(191u) && read_rows(188u)) {
m = 188u;
plausible = true;
} }
bool front_all = false; bool front_all = false;
const int rigid_score = const int rigid_score =
@ -5097,6 +5146,26 @@ void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t
g_dynobj_rows[7] = LoadGuestF32(base, ps_bank + (15 * 4 + 3) * 4); g_dynobj_rows[7] = LoadGuestF32(base, ps_bank + (15 * 4 + 3) * 4);
g_dynobj_rows[8] = mult; g_dynobj_rows[8] = mult;
g_dynobj_rows[9] = LoadGuestF32(base, ps_bank + (8 * 4 + 3) * 4); g_dynobj_rows[9] = LoadGuestF32(base, ps_bank + (8 * 4 + 3) * 4);
// Static world-shadow transform (c5 = u row, c6 = v row,
// c7 = ray depth row): consumed by the native world-shadow
// re-render + the PS world term. Region-stable rows; a change
// re-primes the native map (see RenderShadowAtlas).
float ws[12];
bool ws_ok = true;
float uvmag = 0.0f;
for (int k = 0; k < 12; ++k) {
ws[k] = LoadGuestF32(base, ps_bank + ((5 + k / 4) * 4 + k % 4) * 4);
if (!(ws[k] == ws[k]) || std::fabs(ws[k]) > 1e6f) {
ws_ok = false;
}
if (k < 8 && (k % 4) < 3) {
uvmag += std::fabs(ws[k]);
}
}
if (ws_ok && uvmag > 1e-6f) {
std::memcpy(g_dynobj_ws, ws, sizeof(ws));
g_dynobj_ws_have = true;
}
if (!g_dynobj_have) { if (!g_dynobj_have) {
REXLOG_INFO( REXLOG_INFO(
"native-scene: dynamicobject rows captured sun=({:.3f},{:.3f}," "native-scene: dynamicobject rows captured sun=({:.3f},{:.3f},"
@ -9304,6 +9373,10 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
if (g_dynobj_have) { if (g_dynobj_have) {
std::memcpy(scene.dynobj_rows, g_dynobj_rows, sizeof(g_dynobj_rows)); std::memcpy(scene.dynobj_rows, g_dynobj_rows, sizeof(g_dynobj_rows));
scene.dynobj_valid = true; scene.dynobj_valid = true;
if (g_dynobj_ws_have) {
std::memcpy(scene.dynobj_ws, g_dynobj_ws, sizeof(g_dynobj_ws));
scene.dynobj_ws_valid = true;
}
} }
if (g_sky_have) { if (g_sky_have) {
scene.sky_height = g_sky_height; scene.sky_height = g_sky_height;
@ -10324,10 +10397,26 @@ struct RendererState {
uint32_t shadow_dump_enqueued = 0; uint32_t shadow_dump_enqueued = 0;
uint32_t shadow_dump_written = 0; uint32_t shadow_dump_written = 0;
bool shadow_dump_done = false; bool shadow_dump_done = false;
// Per-frame shadow constant buffer ring (CBV b1). // Per-frame shadow constant buffer ring (CBV b1). 512-byte slices: the
// first 256 bytes are the original 16-row block, rows 16-18 carry the
// dynamicobject world-shadow transform (dyn_ws* in the scene shader).
static constexpr uint32_t kShadowCbRegions = 8; static constexpr uint32_t kShadowCbRegions = 8;
static constexpr uint32_t kShadowCbSlice = 512;
nrhi::Buffer* shadow_cb = nullptr; nrhi::Buffer* shadow_cb = nullptr;
uint8_t* shadow_cb_cpu = nullptr; uint8_t* shadow_cb_cpu = nullptr;
// dynamicobject static world-shadow map (512x512, same convention as the
// 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
// transform. MIN blend accumulates across frames WITHOUT clearing; the
// per-frame item list is view-culled, and dropping off-screen casters
// would flicker props' shade with the camera. Cleared (re-primed) only
// when the captured transform changes (streaming region switch).
nrhi::Texture* world_shadow = nullptr;
nrhi::TextureView* world_shadow_srv = nullptr;
bool world_shadow_in_srv = false;
bool world_shadow_primed = false;
float world_shadow_rows[12] = {};
static constexpr uint32_t kWorldShadowSize = 512;
static constexpr uint32_t kUiRegionSize = 1u << 20; static constexpr uint32_t kUiRegionSize = 1u << 20;
static constexpr uint32_t kUiRegions = 8; static constexpr uint32_t kUiRegions = 8;
nrhi::Buffer* ui_ring = nullptr; nrhi::Buffer* ui_ring = nullptr;
@ -10818,6 +10907,11 @@ bool DecodeMesh(nrhi::Device* device, uint8_t* base, const DrawItem& item,
out[1] = float(iy) / 1023.0f; out[1] = float(iy) / 1023.0f;
out[2] = float(iz) / 511.0f; out[2] = float(iz) / 511.0f;
}; };
// Stored-tangent-frame pack (world-shading / dynobj v2): snorm
// component to the unorm byte of the blend-weight word.
const auto pk = [](float f) {
return uint32_t(std::lround((f * 0.5f + 0.5f) * 255.0f)) & 0xFFu;
};
float n3[3] = {0.0f, 0.0f, 0.0f}; float n3[3] = {0.0f, 0.0f, 0.0f};
if (item.env_family != 0 && if (item.env_family != 0 &&
(item.env_family <= 6 || item.env_family == 13) && (item.env_family <= 6 || item.env_family == 13) &&
@ -10848,9 +10942,6 @@ bool DecodeMesh(nrhi::Device* device, uint8_t* base, const DrawItem& item,
if (!item.skinned && item.normal_fmt == 16) { if (!item.skinned && item.normal_fmt == 16) {
float b3[3]; float b3[3];
unpack_10_11_11(item.normal_offset, b3); unpack_10_11_11(item.normal_offset, b3);
const auto pk = [](float f) {
return uint32_t(std::lround((f * 0.5f + 0.5f) * 255.0f)) & 0xFFu;
};
// w byte is a three-state presence sentinel: 0 = no stored frame // w byte is a three-state presence sentinel: 0 = no stored frame
// (meshes that skip this pack leave the word zero), 100 = negative // (meshes that skip this pack leave the word zero), 100 = negative
// handedness, 200 = positive. // handedness, 200 = positive.
@ -10866,6 +10957,24 @@ bool DecodeMesh(nrhi::Device* device, uint8_t* base, const DrawItem& item,
// interior verts where t is parallel to b and the cross vanishes, // interior verts where t is parallel to b and the cross vanishes,
// measured in capture, so the stored normal is used.) // measured in capture, so the stored normal is used.)
unpack_10_11_11(item.normal_offset, n3); unpack_10_11_11(item.normal_offset, n3);
// Dynamicobject v2: rigid props store a full usage-6/7 tangent +
// binormal pair alongside the usage-3 normal. Pack the binormal plus
// the handedness relating cross(B, N) to the stored tangent into the
// unused blend-weight bytes (same sentinel encoding as the env
// families above); the authored frame carries the per-UV-island
// mirror decisions the screen-space fallback cannot know.
if (!item.skinned && item.dynobj != 0 && item.tb_fmt == 16) {
float t3[3], b3[3];
unpack_10_11_11(item.tangent_offset, t3);
unpack_10_11_11(item.binormal_offset, b3);
const float h = (b3[1] * n3[2] - b3[2] * n3[1]) * t3[0] +
(b3[2] * n3[0] - b3[0] * n3[2]) * t3[1] +
(b3[0] * n3[1] - b3[1] * n3[0]) * t3[2];
const uint32_t packed = pk(b3[0]) | (pk(b3[1]) << 8) |
(pk(b3[2]) << 16) |
((h >= 0.0f ? 200u : 100u) << 24);
std::memcpy(&dst[v * 14 + 7], &packed, 4);
}
} else if (item.tb_fmt == 16) { } else if (item.tb_fmt == 16) {
// NPC character meshes carry no normal element; the game's VS // NPC character meshes carry no normal element; the game's VS
// derives it as cross(tangent, binormal) (usage 6 x usage 7; verified // derives it as cross(tangent, binormal) (usage 6 x usage 7; verified
@ -10878,11 +10987,31 @@ bool DecodeMesh(nrhi::Device* device, uint8_t* base, const DrawItem& item,
n3[0] = t3[1] * b3[2] - t3[2] * b3[1]; n3[0] = t3[1] * b3[2] - t3[2] * b3[1];
n3[1] = t3[2] * b3[0] - t3[0] * b3[2]; n3[1] = t3[2] * b3[0] - t3[0] * b3[2];
n3[2] = t3[0] * b3[1] - t3[1] * b3[0]; n3[2] = t3[0] * b3[1] - t3[1] * b3[0];
// Dynamicobject v2, tangent+binormal-only layout variant: the derived
// normal makes cross(B, N) == T x |B|^2 up to orthogonality error, so
// the handedness is positive by construction.
if (!item.skinned && item.dynobj != 0) {
const uint32_t packed = pk(b3[0]) | (pk(b3[1]) << 8) |
(pk(b3[2]) << 16) | (200u << 24);
std::memcpy(&dst[v * 14 + 7], &packed, 4);
}
} }
dst[v * 14 + 9] = n3[0]; dst[v * 14 + 9] = n3[0];
dst[v * 14 + 10] = n3[1]; dst[v * 14 + 10] = n3[1];
dst[v * 14 + 11] = n3[2]; dst[v * 14 + 11] = n3[2];
} }
// Dynobj v2 layout telemetry (first few meshes): which tangent-frame
// source the props carry decides between the packed authored frame and
// the shader's screen-space fallback.
if (item.dynobj != 0) {
static std::atomic<int> dynobj_layout_logged{0};
if (dynobj_layout_logged.fetch_add(1, std::memory_order_relaxed) < 8) {
REXLOG_INFO(
"native-scene: dynobj mesh {:08X} layout normal_fmt={} tb_fmt={} "
"skinned={}",
item.mesh, item.normal_fmt, item.tb_fmt, item.skinned);
}
}
// Stretch-veto probe (see g_skin_probe): cache ~32 decoded sample verts // Stretch-veto probe (see g_skin_probe): cache ~32 decoded sample verts
// so the guest thread can cheaply skin what the GPU will actually draw. // so the guest thread can cheaply skin what the GPU will actually draw.
if (item.skinned) { if (item.skinned) {
@ -13330,10 +13459,37 @@ bool EnsureShadowResources(const NativeGuestOutputRenderContext& context) {
g_r.shadow_tile, g_r.shadow_tile, g_r.shadow_tile, g_r.shadow_tile,
tile_cfg > 0 ? "" : ", auto"); tile_cfg > 0 ? "" : ", auto");
} }
if (!g_r.world_shadow && g_r.shadow_raw != nullptr) {
// dynamicobject static world-shadow map (see RendererState). Fixed at
// the game's own 512x512: a coarse region-scale baked-shade map whose
// PCF footprint the props' shading was tuned against.
nrhi::TextureDesc desc;
desc.width = RendererState::kWorldShadowSize;
desc.height = RendererState::kWorldShadowSize;
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.world_shadow = device->CreateTexture(desc);
if (g_r.world_shadow != nullptr) {
nrhi::TextureViewDesc vd;
vd.mip_levels = 1;
g_r.world_shadow_srv = device->CreateTextureView(g_r.world_shadow, vd);
}
if (g_r.world_shadow_srv == nullptr) {
REXLOG_ERROR("native-scene: world-shadow map creation failed");
g_r.failed = true;
return false;
}
g_r.world_shadow_in_srv = false;
g_r.world_shadow_primed = false;
}
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.
g_r.shadow_cb = CreateUploadBuffer(device, 256u * RendererState::kShadowCbRegions); g_r.shadow_cb = CreateUploadBuffer(
device, size_t(RendererState::kShadowCbSlice) * RendererState::kShadowCbRegions);
g_r.shadow_cb_cpu = g_r.shadow_cb_cpu =
g_r.shadow_cb ? static_cast<uint8_t*>(device->Map(g_r.shadow_cb)) g_r.shadow_cb ? static_cast<uint8_t*>(device->Map(g_r.shadow_cb))
: nullptr; : nullptr;
@ -14339,12 +14495,14 @@ void WarmItemResources(const NativeGuestOutputRenderContext& context, uint8_t* b
if (REXCVAR_GET(skate3_native_render_scene_macro)) { if (REXCVAR_GET(skate3_native_render_scene_macro)) {
warm_texture(item.macro_tex); warm_texture(item.macro_tex);
} }
if (item.water || item.char_family >= 6 || if (item.water || item.char_family >= 6 || item.dynobj != 0 ||
(item.env_family >= 1 && item.env_family <= 6)) { (item.env_family >= 1 && item.env_family <= 6)) {
// v2: fams 1-4 sample their base normal map per-pixel too. // v2: fams 1-4 and the dynobj families sample their base normal map
// per-pixel too.
warm_texture(item.water_normal); warm_texture(item.water_normal);
} }
if (item.env_family >= 1 && item.env_family <= 4 && item.env_family != 2) { if ((item.env_family >= 1 && item.env_family <= 4 && item.env_family != 2) ||
item.dynobj != 0) {
// v2 detail normal map (t8; fam 2 carries no base+detail pair). // v2 detail normal map (t8; fam 2 carries no base+detail pair).
warm_texture(item.detail_tex); warm_texture(item.detail_tex);
} }
@ -14355,9 +14513,10 @@ void WarmItemResources(const NativeGuestOutputRenderContext& context, uint8_t* b
if (item.char_family >= 4 && item.char_family <= 5) { if (item.char_family >= 4 && item.char_family <= 5) {
warm_texture(item.hair_alpha_tex); warm_texture(item.hair_alpha_tex);
} }
if ((item.env_family != 0 && item.env_family != 10) || item.unlit) { if ((item.env_family != 0 && item.env_family != 10) || item.unlit ||
// unlit = sky: spec_tex is the 1D sun gradient. Decal fams (3/4) now item.dynobj != 0) {
// consume their spec masks too (v2, t9). // unlit = sky: spec_tex is the 1D sun gradient. Decal fams (3/4) and
// the dynobj families consume their spec masks too (v2, t9).
warm_texture(item.spec_tex); warm_texture(item.spec_tex);
} }
// Environment cube (negative-cached like the draw path). // Environment cube (negative-cached like the draw path).
@ -14570,12 +14729,14 @@ void ProcessPrewarmEntry(uint8_t* base, const PrewarmEntry& e) {
if (REXCVAR_GET(skate3_native_render_scene_macro)) { if (REXCVAR_GET(skate3_native_render_scene_macro)) {
stage_texture(item.macro_tex); stage_texture(item.macro_tex);
} }
if (item.water || item.char_family >= 6 || if (item.water || item.char_family >= 6 || item.dynobj != 0 ||
(item.env_family >= 1 && item.env_family <= 6)) { (item.env_family >= 1 && item.env_family <= 6)) {
// v2: fams 1-4 sample their base normal map per-pixel too. // v2: fams 1-4 and the dynobj families sample their base normal map
// per-pixel too.
stage_texture(item.water_normal); stage_texture(item.water_normal);
} }
if (item.env_family >= 1 && item.env_family <= 4 && item.env_family != 2) { if ((item.env_family >= 1 && item.env_family <= 4 && item.env_family != 2) ||
item.dynobj != 0) {
// v2 detail normal map (t8; fam 2 carries no base+detail pair). // v2 detail normal map (t8; fam 2 carries no base+detail pair).
stage_texture(item.detail_tex); stage_texture(item.detail_tex);
} }
@ -14585,9 +14746,10 @@ void ProcessPrewarmEntry(uint8_t* base, const PrewarmEntry& e) {
if (item.char_family >= 4 && item.char_family <= 5) { if (item.char_family >= 4 && item.char_family <= 5) {
stage_texture(item.hair_alpha_tex); stage_texture(item.hair_alpha_tex);
} }
if ((item.env_family != 0 && item.env_family != 10) || item.unlit) { if ((item.env_family != 0 && item.env_family != 10) || item.unlit ||
// unlit = sky: spec_tex is the 1D sun gradient. Decal fams (3/4) now item.dynobj != 0) {
// consume their spec masks too (v2, t9). // unlit = sky: spec_tex is the 1D sun gradient. Decal fams (3/4) and
// the dynobj families consume their spec masks too (v2, t9).
stage_texture(item.spec_tex); stage_texture(item.spec_tex);
} }
@ -16253,6 +16415,122 @@ bool RenderShadowAtlas(const NativeGuestOutputRenderContext& context,
} }
} }
} }
// ---- dynamicobject static world-shadow map ----
// The game's props sample a 512x512 baked-shade depth map (tf1) the guest
// never renders in native mode (its memory stays zeroed). Re-render it
// natively from the frame's STATIC world items with the captured c5/c6/c7
// projection, ps_shadow_caster convention (x = light-space depth, MIN
// blend). The map ACCUMULATES across frames without clearing: the item
// list is view-culled, and dropping off-screen casters would flicker
// props' shade with the camera; static geometry seen once stays in the
// map until the transform changes (streaming region switch), which
// re-primes it.
if (g_r.world_shadow != nullptr && g_r.pso_shadow_caster != nullptr &&
debug_mode == 0 && scene.dynobj_ws_valid &&
REXCVAR_GET(skate3_native_render_scene_shadows) &&
REXCVAR_GET(skate3_native_render_scene_dynobj_v2)) {
bool changed = !g_r.world_shadow_primed;
for (int k = 0; k < 12 && !changed; ++k) {
changed = std::fabs(scene.dynobj_ws[k] - g_r.world_shadow_rows[k]) >
1e-4f * std::max(1.0f, std::fabs(g_r.world_shadow_rows[k]));
}
static uint64_t ws_pass_counter = 0;
++ws_pass_counter;
// Accumulation cadence: a full static-item pass every 4th frame keeps
// newly streamed / newly visible geometry flowing into the map at
// negligible steady-state cost; a transform change renders immediately.
if (changed || (ws_pass_counter & 3u) == 0) {
if (g_r.world_shadow_in_srv) {
cmd->Barrier(g_r.world_shadow, nrhi::ResourceState::kPixelShaderResource,
nrhi::ResourceState::kRenderTarget);
cmd->FlushBarriers();
g_r.world_shadow_in_srv = false;
}
if (changed) {
const float ws_clear[4] = {1.0f, 1.0f, 0.0f, 0.0f};
cmd->ClearRenderTarget(g_r.world_shadow, ws_clear);
std::memcpy(g_r.world_shadow_rows, scene.dynobj_ws,
sizeof(g_r.world_shadow_rows));
if (!g_r.world_shadow_primed) {
REXLOG_INFO(
"native-scene: world-shadow map primed (dynobj c5/c6/c7 rows)");
}
g_r.world_shadow_primed = true;
}
// View-proj columns = the game's own projection rows: clip.x/y/z =
// dot(wp4, c5/c6/c7), clip.w = 1 (same construction as the CSM
// lightvp above).
float wsvp[16];
for (int r = 0; r < 3; ++r) {
wsvp[r * 4 + 0] = scene.dynobj_ws[r];
wsvp[r * 4 + 1] = scene.dynobj_ws[4 + r];
wsvp[r * 4 + 2] = scene.dynobj_ws[8 + r];
wsvp[r * 4 + 3] = 0.0f;
}
wsvp[12] = scene.dynobj_ws[3];
wsvp[13] = scene.dynobj_ws[7];
wsvp[14] = scene.dynobj_ws[11];
wsvp[15] = 1.0f;
cmd->SetRenderTargets(g_r.world_shadow, nullptr);
cmd->SetBindingLayout(g_r.layout);
cmd->SetPipeline(g_r.pso_shadow_caster);
cmd->SetConstantBuffer(9, g_r.bone_ring, bone_region);
const float ws_size = float(RendererState::kWorldShadowSize);
cmd->SetViewport(nrhi::Viewport{0.0f, 0.0f, ws_size, ws_size, 0.0f, 1.0f});
cmd->SetScissor(nrhi::Rect{0, 0, int32_t(RendererState::kWorldShadowSize),
int32_t(RendererState::kWorldShadowSize)});
for (const DrawItem& item : scene.items) {
// STATIC world geometry only: the game's map bakes buildings/trees/
// ground. Dynamic content (characters, vehicles, cloth, movable
// props) casts through the CSM instead, accumulating a movable
// prop here would freeze its shade at a stale position.
if (item.transparent || item.unlit || item.cloth_quads || item.water ||
item.skinned || item.ropa || item.char_family != 0 ||
item.dynobj != 0) {
continue;
}
auto mit = g_r.meshes.find(item.mesh);
if (mit == g_r.meshes.end()) {
continue;
}
float constants[52] = {};
std::memcpy(constants, item.world, sizeof(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 += item.world[r * 4 + k] * wsvp[k * 4 + col];
}
mvp[r * 4 + col] = sum;
}
}
cmd->SetRootConstants(0, 52, constants, 0);
cmd->SetBufferSrv(3, g_r.bone_ring, bone_region);
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 : 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);
}
}
cmd->Barrier(g_r.world_shadow, nrhi::ResourceState::kRenderTarget,
nrhi::ResourceState::kPixelShaderResource);
cmd->FlushBarriers();
g_r.world_shadow_in_srv = true;
}
}
*out_draws = shadow_draws; *out_draws = shadow_draws;
return shadow_ready; return shadow_ready;
} }
@ -16848,9 +17126,10 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
// shader is also gated by sh_misc.y). // shader is also gated by sh_misc.y).
if (g_r.shadow_cb != nullptr) { if (g_r.shadow_cb != nullptr) {
const uint32_t cb_offset = const uint32_t cb_offset =
uint32_t(frame_number % RendererState::kShadowCbRegions) * 256u; uint32_t(frame_number % RendererState::kShadowCbRegions) *
RendererState::kShadowCbSlice;
float* cb = reinterpret_cast<float*>(g_r.shadow_cb_cpu + cb_offset); float* cb = reinterpret_cast<float*>(g_r.shadow_cb_cpu + cb_offset);
std::memset(cb, 0, 256); std::memset(cb, 0, RendererState::kShadowCbSlice);
if (shadow_ready) { if (shadow_ready) {
std::memcpy(cb + 0, sh + 0, 4 * sizeof(float)); // sh_x = PS c0 std::memcpy(cb + 0, sh + 0, 4 * sizeof(float)); // sh_x = PS c0
std::memcpy(cb + 4, sh + 12, 4 * sizeof(float)); // sh_y = PS c3 std::memcpy(cb + 4, sh + 12, 4 * sizeof(float)); // sh_y = PS c3
@ -16912,6 +17191,12 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
cb[51] = scene.dynobj_rows[7]; // bounce scale (c15.w) cb[51] = scene.dynobj_rows[7]; // bounce scale (c15.w)
cb[52] = scene.dynobj_rows[8]; // material multiplier (c14.y) cb[52] = scene.dynobj_rows[8]; // material multiplier (c14.y)
cb[53] = scene.dynobj_rows[9]; // static world-shadow floor (c8.w) cb[53] = scene.dynobj_rows[9]; // static world-shadow floor (c8.w)
// Static world-shadow transform rows (dyn_wsx/dyn_wsy/dyn_wsz,
// cb[64..75]): consumed only when a draw carries the misc.z world-
// shadow bind flag (the map primed and bound at t4).
if (scene.dynobj_ws_valid) {
std::memcpy(cb + 64, scene.dynobj_ws, sizeof(scene.dynobj_ws));
}
} }
cmd->SetConstantBuffer(6, g_r.shadow_cb, cb_offset); cmd->SetConstantBuffer(6, g_r.shadow_cb, cb_offset);
// b2 (character lighting) default: point at the ring base so the root // b2 (character lighting) default: point at the ring base so the root
@ -17899,6 +18184,49 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
} }
} }
} }
// Dynamicobject v2: the same misc.z/w convention and t5/t8/t9 slots as
// fams 1-4; dynobj draws are never transparent/water and carry
// env_family 0, so both misc slots are free, and t4 stays on its white
// fallback (the spec-mask t4 bind is env-family-only), so the t5 pair
// costs nothing. The spec/ecc masks go to t9 rather than t4: it keeps
// t4's semantics untouched and matches the decal families' 2-channel
// mask convention. Gated exactly like the PS branch selection (dynobj
// frame rows captured) plus the dynobj_v2 cvar for A/B against the v1
// flat response.
if (item.dynobj != 0 && debug_mode == 0 && scene.dynobj_valid &&
REXCVAR_GET(skate3_native_render_scene_dynobj_v2)) {
if (item.water_normal != 0) {
const GuestTexture* nrm = resolve_texture(item.water_normal, 3);
if (nrm != &g_r.white && nrm->valid && nrm->srv_mips != 0) {
pair_normal = nrm;
normal_paired = true;
v2_flags |= 1;
}
}
if ((v2_flags & 1u) != 0 && item.detail_tex != 0 &&
item.detail_scale > 0.0f) {
const GuestTexture* det = resolve_texture(item.detail_tex, 7);
if (det != &g_r.white && det->valid && det->srv_mips != 0) {
v2_detail = det;
v2_flags |= 2;
}
}
if (item.spec_tex != 0) {
const GuestTexture* sp = resolve_texture(item.spec_tex, 6);
if (sp != &g_r.white && sp->valid && sp->srv_mips != 0) {
v2_spec2 = sp;
v2_flags |= 4;
}
}
// Static world-shadow map at t4 (flag 8): the native re-render of the
// game's baked-shade map (see RenderShadowAtlas). t4 is otherwise the
// white fallback on dynobj draws. Without it the PS world term is 1
// and props inside baked building shade light at full key.
if (g_r.world_shadow_srv != nullptr && g_r.world_shadow_in_srv &&
scene.dynobj_ws_valid) {
v2_flags |= 8;
}
}
constants[44] = item.macro_scale; constants[44] = item.macro_scale;
constants[45] = item.macro_opacity; constants[45] = item.macro_opacity;
constants[46] = macro_tex != &g_r.white ? 1.0f : 0.0f; constants[46] = macro_tex != &g_r.white ? 1.0f : 0.0f;
@ -17945,7 +18273,7 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
std::memcpy(constants + 40, scene.fog_color, 4 * sizeof(float)); std::memcpy(constants + 40, scene.fog_color, 4 * sizeof(float));
} else if (v2_flags != 0) { } else if (v2_flags != 0) {
// misc.z = the v2 material bind flags, misc.w = detailNormalUVScale // misc.z = the v2 material bind flags, misc.w = detailNormalUVScale
// (fams 1-4 opaque; see the v2 block above). // (opaque fams 1-4 and the dynobj families; see the v2 blocks above).
constants[50] = float(v2_flags); constants[50] = float(v2_flags);
constants[51] = item.detail_scale; constants[51] = item.detail_scale;
} else if ((item.env_family >= 5 && item.env_family <= 6) || } else if ((item.env_family >= 5 && item.env_family <= 6) ||
@ -18048,8 +18376,14 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
cmd->SetTexture(1, diffuse->srv); cmd->SetTexture(1, diffuse->srv);
cmd->SetTexture(2, (lightmap != nullptr ? lightmap : &g_r.white)->srv); cmd->SetTexture(2, (lightmap != nullptr ? lightmap : &g_r.white)->srv);
cmd->SetTexture(4, macro_tex->srv); cmd->SetTexture(4, macro_tex->srv);
if (normal_paired) { // t4 override: the dynobj world-shadow map rides the free first entry
cmd->SetTexturePair(5, decal_tex->srv, pair_normal->srv); // of the t4/t5 pair table (flag 8: dynobj draws never bind decal art
// or spec masks there).
nrhi::TextureView* t4_view =
(v2_flags & 8u) != 0 ? g_r.world_shadow_srv : decal_tex->srv;
if (normal_paired || t4_view != decal_tex->srv) {
cmd->SetTexturePair(5, t4_view,
normal_paired ? pair_normal->srv : g_r.white.srv);
} else { } else {
cmd->SetTexture(5, decal_tex->srv); cmd->SetTexture(5, decal_tex->srv);
} }
@ -18736,7 +19070,8 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
cmd->SetBindingLayout(g_r.layout); cmd->SetBindingLayout(g_r.layout);
if (g_r.shadow_cb != nullptr) { if (g_r.shadow_cb != nullptr) {
const uint32_t cb_offset = const uint32_t cb_offset =
uint32_t(frame_number % RendererState::kShadowCbRegions) * 256u; uint32_t(frame_number % RendererState::kShadowCbRegions) *
RendererState::kShadowCbSlice;
cmd->SetConstantBuffer(6, g_r.shadow_cb, cb_offset); cmd->SetConstantBuffer(6, g_r.shadow_cb, cb_offset);
cmd->SetConstantBuffer(9, g_r.bone_ring, bone_region); cmd->SetConstantBuffer(9, g_r.bone_ring, bone_region);
} }
@ -19129,7 +19464,8 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
cmd->SetBindingLayout(g_r.layout); cmd->SetBindingLayout(g_r.layout);
if (g_r.shadow_cb != nullptr) { if (g_r.shadow_cb != nullptr) {
const uint32_t cb_offset = const uint32_t cb_offset =
uint32_t(frame_number % RendererState::kShadowCbRegions) * 256u; uint32_t(frame_number % RendererState::kShadowCbRegions) *
RendererState::kShadowCbSlice;
cmd->SetConstantBuffer(6, g_r.shadow_cb, cb_offset); cmd->SetConstantBuffer(6, g_r.shadow_cb, cb_offset);
cmd->SetConstantBuffer(9, g_r.bone_ring, bone_region); cmd->SetConstantBuffer(9, g_r.bone_ring, bone_region);
} }

View file

@ -303,6 +303,17 @@ struct FrameScene {
// Consumed by the exact dynamicobject scene PS branch. // Consumed by the exact dynamicobject scene PS branch.
bool dynobj_valid = false; bool dynobj_valid = false;
float dynobj_rows[10] = {}; float dynobj_rows[10] = {};
// dynamicobject.fx static world-shadow transform (PS c5/c6/c7, captured
// with the rows above): world-space projection rows into the game's
// 512x512 baked-shade map (uv = dot(wp4, c5/c6) * (+-0.5) + 0.5, ray
// depth = dot(wp4, c7)). The guest never renders that map in native mode
// (its memory stays zeroed), so the native path re-renders it from the
// frame's STATIC world items with these rows (see RenderShadowAtlas) and
// props sample it exactly like the game's PS: shadow = min(csm * gate,
// max(world, c8.w)); without it props inside baked building shade lit
// at full key (the newspaper-machine mint brightening).
bool dynobj_ws_valid = false;
float dynobj_ws[12] = {};
// Character-receiver per-cascade CSM receive biases, captured from a // Character-receiver per-cascade CSM receive biases, captured from a
// validated fam-2 character PIXEL bank (gameplay c8 / editor c9: one row // validated fam-2 character PIXEL bank (gameplay c8 / editor c9: one row
// below the light row; frame-global, e.g. (0.005, 0.014, 0.020)). The // below the light row; frame-global, e.g. (0.005, 0.014, 0.020)). The