render: world shading v2 - per-pixel normal/detail/spec maps on env families

This commit is contained in:
mchughalex 2026-07-17 09:19:30 +01:00
parent 64fedeb7f0
commit a64c9afb9a
4 changed files with 2317 additions and 1927 deletions

View file

@ -58,6 +58,10 @@ cbuffer S : register(b1) {
// (cacstamp_skin_nisPS port; see SampleCsmShadow). sh_misc.zw carry the
// atlas dimensions for the point-snapped taps.
float4 sh_char;
// World-shading v2: x = stored-tangent polarity (the sign relating
// cross(binormal, normal) x handedness to the game's tangent; +-1,
// cvar-tunable against the emulated reference), yzw spare.
float4 sh_v2;
};
// Character-family lighting (defaultcharacter.fx and friends): canonical
// per-draw rows captured from the guest PIXEL constant bank at palette
@ -84,6 +88,12 @@ Texture2D<float4> decal_art : register(t4);
Texture2D<float4> normal_map : register(t5);
Texture2D<float2> shadow_atlas : register(t7);
TextureCube<float4> env_cube : register(t6);
// World-shading v2 material maps (the t8/t9 pair table): the environment
// families' detail normal map (t8, sampled at uv * misc.w) and the decal
// families' spec/ecc masks (t9). Sampled only when the misc.z bind flags
// say so (fams 1-4; see the env branch).
Texture2D<float4> detail_map : register(t8);
Texture2D<float4> spec2_map : register(t9);
// Raw bone palette: 3 float4 rows per bone, column-vector affine [R | t],
// applied with explicit dots (StructuredBuffer<float4x4> default packing is
// column-major and would silently transpose the matrices).
@ -101,6 +111,11 @@ struct VSOut {
float2 uv2 : TEXCOORD2;
float3 nrm : TEXCOORD3;
float2 uv3 : TEXCOORD4;
// Stored tangent frame (world-shading v2): xyz = the mesh's authored
// binormal (world-rotated; zero when the mesh carries none), w = the
// tangent handedness sign. Rides the static meshes' otherwise-unused
// blend-weight bytes (see DecodeMesh).
float4 tanb : TEXCOORD5;
};
VSOut vs_main(float3 p : POSITION, float2 uv : TEXCOORD0, float2 uv2 : TEXCOORD1,
float4 bw : BLENDWEIGHT0, uint4 bi : BLENDINDICES0,
@ -134,6 +149,13 @@ VSOut vs_main(float3 p : POSITION, float2 uv : TEXCOORD0, float2 uv2 : TEXCOORD1
o.uv2 = uv2;
o.nrm = n;
o.uv3 = uv3;
// Stored binormal + handedness from the blend-weight bytes (statics
// only; skinned items use those bytes for actual weights). w passes
// RAW: ~0 = no stored frame, ~0.39 = negative handedness, ~0.78 =
// positive (the DecodeMesh sentinel bytes 0/100/200).
float3 sb = bw.xyz * 2.0 - 1.0;
o.tanb = tint.g > 0.0 ? float4(0.0, 0.0, 0.0, 0.0)
: float4(mul(sb, (float3x3)world), bw.w);
return o;
}
// Native CSM atlas sample at a world position: finest covering cascade,
@ -542,16 +564,17 @@ float4 ps_main(VSOut i) : SV_Target {
float3 lmg = lightmap.SampleLevel(smp_clamp, i.uv2, 0.0).rgb;
// F12 isolation mode 7: visualize the RAW lightmap sample; mode 8:
// visualize the lightmap unwrap coordinate (frac(uv2*16) in rg).
// Debug-only taps on fams 5/6/13 (misc.z is 0 on the other fams).
if (abs(misc.z - 7.0) < 0.5) {
// Debug taps live on fams 5/6/13 only; on fams 1-4 misc.z carries
// the v2 material bind flags instead (family-gated below).
if (fam > 4.5 && abs(misc.z - 7.0) < 0.5) {
return float4(lmg, 1.0);
}
if (abs(misc.z - 8.0) < 0.5) {
if (fam > 4.5 && abs(misc.z - 8.0) < 0.5) {
return float4(frac(i.uv2 * 16.0), 0.0, 1.0);
}
// Mode 9: lightmap-resolve status; RED = a real lightmap is bound
// (tint.r set by the C++ resolve), BLUE = white fallback in t1.
if (abs(misc.z - 9.0) < 0.5) {
if (fam > 4.5 && abs(misc.z - 9.0) < 0.5) {
return float4(tint.r, 0.0, 1.0 - tint.r, 1.0);
}
// tint.r == 0 = the real lightmap has not resolved yet (first-sight
@ -560,21 +583,94 @@ float4 ps_main(VSOut i) : SV_Target {
// patches for the decode window (the ramp-stencil "black square
// flash"); serve unshadowed brightness until the real page lands.
float3 lml = tint.r > 0.0 ? min(lmg * lmg, s + sh_color.rgb) : lmg * lmg;
// GetTangentLight with the neutral (flat) normal map:
// 0.39 * 2.39562 exactly. Fam 13 has no kd term at all; its body is
// D^2 * lml * ALPHA, premultiplied once in the shader on top of the
// a^2 blend factor (verified 0.0-error vs the ucode).
lin = fam > 12.5 ? lml * dcol * albedo.a : lml * 0.93429 * dcol;
if (overlay.w > 2.5) {
// spec/ecc/refmask at t4: phong vs the FIXED literal light
// (-0.14, 0.5, 0.9), power 10 + 290*ecc, tint (2.1, 1.8, 1.5),
// scaled by the clamped lightmap green and the spec mask.
float4 masks = decal_art.Sample(smp, i.uv);
float3 wn = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
// misc.z = 3 (F12 reflection isolation): force the flat normal.
if (overlay.w > 3.5 && abs(misc.z - 3.0) > 0.5) {
// GetTangentLight (world-shading v2). vnd is the tangent-space mapped
// normal from the material's base normal map (t5) + detail map (t8 at
// uv * misc.w): xy = 2*base + 2*detail - 2, z = 2*base.z - 1.
// baseenvironment/decal normalize vnd for the kd dot; default-
// environment (plain normal map, no detail pair) uses it raw. Fams
// 1-4 carry bind flags in misc.z (1 = base normal at t5, 2 = detail
// at t8, 4 = spec2ch at t9); fams 5/6 derive vnd from the reflective
// path's nt below. Without a normal map the exact flat-map fold
// 0.39 * 2.39562 = 0.93429 applies (the v1 constant).
uint v2f = fam < 4.5 ? (uint)(misc.z + 0.5) : 0u;
float kd = 0.93429;
float3 wn = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
float3 vnd_raw = float3(0.0, 0.0, 1.0); // wn mapping (max-z clamped)
float3 vnd = float3(0.0, 0.0, 1.0); // kd dot (family-normalized)
bool have_vnd = false;
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);
vnd_raw = float3(nmv.xy * 2.0 + dxy * 2.0 - 2.0, nmv.z * 2.0 - 1.0);
vnd = (fam < 1.5 || fam > 2.5) ? normalize(vnd_raw) : vnd_raw;
have_vnd = true;
}
// Tangent frames shared by kd and the reflective normal mapping.
// Two variants of the same construction:
// - tt_s/bb_s = the RAW screen-space UV-gradient frame. Exact for
// the 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.
// - tt/bb = analytic world-up axes carrying the screen frame's
// signs; degree-accurate where the mapping IS world-up aligned
// (building facades, calibrated for the glass reflections).
// Transferring signs onto misaligned axes is METASTABLE when the
// UV grid sits ~90 deg off world-up: the transfer dots cross zero
// along a curved wall and kd banded light/dark around the bowl
// transition (seen in a matched F11 capture pair), which is why
// kd and the fam 1-4 normal mapping use the screen frame instead.
float3 tt = float3(1.0, 0.0, 0.0), bb = float3(0.0, 1.0, 0.0);
float3 tt_s = tt, bb_s = bb;
if (have_vnd || (overlay.w > 3.5 && abs(misc.z - 3.0) > 0.5)) {
float3 dp1 = ddx(i.rpos), dp2 = ddy(i.rpos);
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;
bb_s = bb;
float3 bb2 = float3(0.0, 1.0, 0.0) - wn * wn.y;
float lb2 = length(bb2);
if (lb2 > 0.05) {
bb2 /= lb2;
float3 tt2 = cross(bb2, wn);
tt = tt2 * (dot(tt2, tt) >= 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
// i.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). 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) {
// The mapped world normal feeds the spec mirror below; matte spec
// is broad-lobed, so exact axis precision matters less than the
// per-island signs (the analytic substitution exists for
// mirror-sharp glass).
wn = normalize(vnd_raw.x * kt + vnd_raw.y * kb +
wn * max(vnd_raw.z, 0.05));
kd = (vnd.x * 0.58 * sign(dot(kt, sh_sun.xyz)) +
vnd.y * 0.62 * sign(dot(kb, sh_sun.xyz)) +
vnd.z * 0.39) * 2.39562;
}
// misc.z = 3 (F12 reflection isolation): force the flat normal.
// Only the reflective families (5/6/13) ever carry overlay.w == 4;
// fams 1-4 signal their normal map through the misc.z flags above.
if (overlay.w > 3.5 && abs(misc.z - 3.0) > 0.5) {
// Per-pixel normal map (t5, paired descriptor): the real PS
// (baseenvironmentreflective_defaultPS) reflects off the
// normal-mapped normal: tangent normal = 2*(n + detail - 1) on
@ -608,46 +704,29 @@ float4 ps_main(VSOut i) : SV_Target {
float2 trim = float2(misc.x - trim_yi * 1000.0 - 500.0, trim_yi - 500.0) *
0.001;
float3 nt = float3(nmv.xy * 2.0 - 1.0 + trim, nmv.z * 2.0 - 1.0);
float3 dp1 = ddx(i.rpos), dp2 = ddy(i.rpos);
float2 du1 = ddx(i.uv), du2 = ddy(i.uv);
float3 dp2p = cross(dp2, wn), dp1p = cross(wn, dp1);
float3 tt = dp2p * du1.x + dp1p * du2.x;
float3 bb = dp2p * du1.y + dp1p * du2.y;
// Signs/lengths calibrated against the mesh's REAL stored TBN
// (10_11_11 tangent @ +24, s16 normal xy @ +20, handedness in the
// unwrap sign bits, decoded mesh-wide):
// stored T == +dP/dU on 100% of tris (this formula's tt comes out
// as -dP/dU -> negate), stored B == this bb direction (dot 0.999),
// and the game's rows are UNIT so each axis normalizes SEPARATELY
// - the shared max-normalizer both halved and (with the flip)
// mirrored the horizontal tilt, which pushed the reflection
// DEEPER into the dark cube region instead of off it (first
// deploy looked unchanged). With these signs the reconstruction
// matches the ucode's mapped normal to 4 decimals at the traced
// pixel. An inward geometric wn flips all three terms together;
// reflect() is invariant to that.
tt *= -rsqrt(max(dot(tt, tt), 1e-12));
bb *= rsqrt(max(dot(bb, bb), 1e-12));
// The mesh's STORED TBN (what the game's VS interpolates) is the
// world-up frame, not the exact UV-gradient frame: B = up
// projected into the surface, T = cross(B, N), verified on the
// decoded window vertices (B = (0, 0.999, 0), T = horizontal).
// The screen-space frame lands ~3 deg off it (authored TBN vs UV
// gradients), which rotates reflections by degrees, visible as
// parallax against the emulated frame. Use the analytic axes and
// keep only the screen-space frame's SIGNS (mirrored UV islands
// flip the tangent; derivatives track that, the analytic frame
// cannot). Near-horizontal surfaces keep the screen frame.
float3 bb2 = float3(0.0, 1.0, 0.0) - wn * wn.y;
float lb2 = length(bb2);
if (lb2 > 0.05) {
bb2 /= lb2;
float3 tt2 = cross(bb2, wn);
tt = tt2 * (dot(tt2, tt) >= 0.0 ? 1.0 : -1.0);
bb = bb2 * (dot(bb2, bb) >= 0.0 ? 1.0 : -1.0);
}
// (Frame construction hoisted above; the calibration notes ride
// with it; tt/bb here are the shared axes.)
wn = normalize(nt.x * tt + nt.y * bb + wn * max(nt.z, 0.05));
}
// v2: the reflective families share the kd term; fam 5
// (reflective) is a base-family material and normalizes vnd for
// the dot like baseenvironment; fam 6 (reflective_simple) uses
// it raw like the other _simple family. kt/kb = the shared kd
// axes computed above (stored frame when the mesh carries one),
// built from the pre-mapping geometric normal.
float3 v56 = fam < 5.5 ? normalize(nt) : nt;
kd = (v56.x * 0.58 * sign(dot(kt, sh_sun.xyz)) +
v56.y * 0.62 * sign(dot(kb, sh_sun.xyz)) +
v56.z * 0.39) * 2.39562;
}
// Fam 13 has no kd term at all; its body is D^2 * lml * ALPHA,
// premultiplied once in the shader on top of the a^2 blend factor
// (verified 0.0-error vs the ucode).
lin = fam > 12.5 ? lml * dcol * albedo.a : lml * kd * dcol;
if (overlay.w > 2.5) {
// spec/ecc/refmask at t4: phong vs the FIXED literal light
// (-0.14, 0.5, 0.9), power 10 + 290*ecc, tint (2.1, 1.8, 1.5),
// scaled by the clamped lightmap green and the spec mask.
float4 masks = decal_art.Sample(smp, i.uv);
float3 vd = -normalize(i.rpos);
float3 Ls = float3(-0.14, 0.5, 0.9);
float3 refl = Ls - 2.0 * wn * dot(wn, Ls);
@ -686,6 +765,18 @@ float4 ps_main(VSOut i) : SV_Target {
}
}
}
// v2 decal-family spec (spec/ecc at t9): the same phong-vs-fixed-
// light term as the base families; the decal art occupies t4 on
// fams 3/4, so their masks ride the second pair table.
if ((v2f & 4u) != 0u) {
float2 m2 = spec2_map.Sample(smp, i.uv).rg;
float3 vd2 = -normalize(i.rpos);
float3 Ls2 = float3(-0.14, 0.5, 0.9);
float3 refl2 = Ls2 - 2.0 * wn * dot(wn, Ls2);
float ks2 =
pow(max(saturate(dot(vd2, -refl2)), 1e-6), 10.0 + 290.0 * m2.y);
lin += ks2 * float3(2.1, 1.8, 1.5) * lml.g * m2.x;
}
if (fam > 12.5) {
// Fam 13 blend factor: the PS outputs a^2 (straight-alpha blend on
// top of the in-shader premultiplied body; wisps thin as ~a^3,

File diff suppressed because it is too large Load diff

View file

@ -82,6 +82,19 @@ REXCVAR_DEFINE_INT32(skate3_native_render_scene_msaa, 4, "Skate 3",
"fix texture aliasing.")
.range(1, 8)
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_world_v2_tan_sign, 1.0, "Skate 3",
"World-shading v2: polarity relating the stored frame's "
"cross(binormal, normal) x handedness to the game's tangent "
"(+1/-1). Calibrated against the emulated reference; flips the "
"kd sun-direction response if wrong.")
.range(-1.0, 1.0)
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_world_v2, true, "Skate 3",
"World-shading v2: per-pixel normal/detail/spec maps on the exact "
"environment families (the game's own GetTangentLight kd + phong "
"terms; 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",
"Screen-space ambient occlusion (GTAO) in the native renderer: soft "
"contact shading where surfaces meet (under ledges, rails, vehicles, "
@ -2568,6 +2581,14 @@ bool BuildItemFromMesh(uint8_t* base, uint32_t mesh, DrawItem& item) {
// the animated.tree "noise" tint map, bound at t4 on families
// that carry no decal art (see DrawItem::spec_tex).
slot = &item.spec_tex;
} else if (std::memcmp(text, "detailNormalUVScale", 20) == 0) {
// Shader-constant channel (the game's VS c8.x, typically 8): the
// detail normal map's UV multiplier, consumed by the v2 kd term.
const float f = std::bit_cast<float>(chan_u32(i, 0x10));
if (f > 0.0f && f < 1e6f) {
item.detail_scale = f;
}
continue;
} else if (std::memcmp(text, "macroOverlayUVScale", 20) == 0 ||
std::memcmp(text, "macroOverlayOpacity", 20) == 0) {
// Shader-constant channel: the float lives in the first guid word.
@ -10817,7 +10838,27 @@ bool DecodeMesh(nrhi::Device* device, uint8_t* base, const DrawItem& item,
n3[1] = ny / 32767.0f;
const float d = 1.0f - n3[0] * n3[0] - n3[1] * n3[1];
n3[2] = (sy > 0 ? 1.0f : -1.0f) * std::sqrt(d > 0.0f ? d : 0.0f);
(void)sx; // sign.x = tangent handedness, unused until normal maps
// World-shading v2: the STORED tangent frame. These static meshes
// never skin, so the blend-weight bytes are free: pack the mesh's
// k_10_11_11 binormal (unorm-mapped) plus the unwrap sign.x tangent
// handedness into them. The shader reconstructs T = cross(B, N) x
// handedness; authoring decides per UV island whether the frame
// follows a mirror, which derivative frames cannot know (the wooden
// ramp panels banded light/dark per island without this).
if (!item.skinned && item.normal_fmt == 16) {
float b3[3];
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
// (meshes that skip this pack leave the word zero), 100 = negative
// handedness, 200 = positive.
const uint32_t packed = pk(b3[0]) | (pk(b3[1]) << 8) |
(pk(b3[2]) << 16) |
((sx > 0 ? 200u : 100u) << 24);
std::memcpy(&dst[v * 14 + 7], &packed, 4);
}
} else if (item.normal_fmt == 16) {
// (Vehicle meshes: the game's VS derives cross(tangent, binormal)
// instead, but the stored usage-3 normal MATCHES it on every vertex
@ -12476,14 +12517,18 @@ bool EnsureRootSignature(const NativeGuestOutputRenderContext& context) {
// overlay.w == 4 (pair bound).
ld.params[5] = {nrhi::BindingParamKind::kTextureTable, 4, 2,
nrhi::Visibility::kPixel};
// Dynamic-shadow additions: per-frame receiver constants (b1) + the
// blurred shadow atlas (t5).
// Dynamic-shadow additions: per-frame receiver constants (b1).
ld.params[6] = {nrhi::BindingParamKind::kConstantBuffer, /*b*/ 1, 1,
nrhi::Visibility::kPixel};
ld.params[7] = {nrhi::BindingParamKind::kTextureTable, 7, 1,
// Environment cube (t6) + blurred shadow atlas (t7) as ONE two-entry
// table (bound together via SetTexturePair); merging them freed the
// root-signature DWORD the v2 material table below needs.
ld.params[7] = {nrhi::BindingParamKind::kTextureTable, 6, 2,
nrhi::Visibility::kPixel};
// Water environment cube (t6): the flowingwater/ocean reflection term.
ld.params[8] = {nrhi::BindingParamKind::kTextureTable, 6, 1,
// World-shading v2 material maps: the detail normal map (t8) + the
// decal families' spec/ecc masks (t9), bound as a pair on draws whose
// misc.z flags enable them.
ld.params[8] = {nrhi::BindingParamKind::kTextureTable, 8, 2,
nrhi::Visibility::kPixel};
// Character lighting block (b2): the canonical per-draw rows captured
// from the guest PS bank (CaptureCharLighting), sliced out of the bone
@ -14294,9 +14339,15 @@ void WarmItemResources(const NativeGuestOutputRenderContext& context, uint8_t* b
if (REXCVAR_GET(skate3_native_render_scene_macro)) {
warm_texture(item.macro_tex);
}
if (item.water || item.char_family >= 6) {
if (item.water || item.char_family >= 6 ||
(item.env_family >= 1 && item.env_family <= 6)) {
// v2: fams 1-4 sample their base normal map per-pixel too.
warm_texture(item.water_normal);
}
if (item.env_family >= 1 && item.env_family <= 4 && item.env_family != 2) {
// v2 detail normal map (t8; fam 2 carries no base+detail pair).
warm_texture(item.detail_tex);
}
if (item.decal && REXCVAR_GET(skate3_native_render_scene_decals)) {
warm_texture(item.decal_art);
warm_fetch_words(item.decal_fetch);
@ -14304,8 +14355,9 @@ void WarmItemResources(const NativeGuestOutputRenderContext& context, uint8_t* b
if (item.char_family >= 4 && item.char_family <= 5) {
warm_texture(item.hair_alpha_tex);
}
if ((item.env_family != 0 && !item.decal && item.env_family != 10) ||
item.unlit) { // unlit = sky: spec_tex is the 1D sun gradient
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
// consume their spec masks too (v2, t9).
warm_texture(item.spec_tex);
}
// Environment cube (negative-cached like the draw path).
@ -14518,17 +14570,24 @@ void ProcessPrewarmEntry(uint8_t* base, const PrewarmEntry& e) {
if (REXCVAR_GET(skate3_native_render_scene_macro)) {
stage_texture(item.macro_tex);
}
if (item.water || item.char_family >= 6) {
if (item.water || item.char_family >= 6 ||
(item.env_family >= 1 && item.env_family <= 6)) {
// v2: fams 1-4 sample their base normal map per-pixel too.
stage_texture(item.water_normal);
}
if (item.env_family >= 1 && item.env_family <= 4 && item.env_family != 2) {
// v2 detail normal map (t8; fam 2 carries no base+detail pair).
stage_texture(item.detail_tex);
}
if (item.decal && REXCVAR_GET(skate3_native_render_scene_decals)) {
stage_texture(item.decal_art);
}
if (item.char_family >= 4 && item.char_family <= 5) {
stage_texture(item.hair_alpha_tex);
}
if ((item.env_family != 0 && !item.decal && item.env_family != 10) ||
item.unlit) { // unlit = sky: spec_tex is the 1D sun gradient
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
// consume their spec masks too (v2, t9).
stage_texture(item.spec_tex);
}
@ -16837,6 +16896,10 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
cb[39] = scene.family_rows[3]; // proxyworld scale (proxy PS c3.y)
std::memcpy(cb + 40, scene.fog_color, 4 * sizeof(float));
}
// World-shading v2 row (sh_v2, cb[60..63]): x = stored-tangent
// polarity.
cb[60] =
float(REXCVAR_GET(skate3_native_render_scene_world_v2_tan_sign));
// dynamicobject.fx frame rows (dyn_sun/dyn_amb/dyn_misc at cb[44..55]).
if (scene.dynobj_valid) {
cb[44] = scene.dynobj_rows[0]; // sun dir (PS c9)
@ -16854,7 +16917,10 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
// b2 (character lighting) default: point at the ring base so the root
// CBV is never left unset; character draws re-point it per item.
cmd->SetConstantBuffer(9, g_r.bone_ring, bone_region);
cmd->SetTexture(7, shadow_ready ? g_r.shadow_srv_final : g_r.white.srv);
// t6/t7 ride one table: default = white cube + this frame's atlas
// (draw_item re-pairs with the item's real cube when one resolves).
cmd->SetTexturePair(7, g_r.white_cube.srv,
shadow_ready ? g_r.shadow_srv_final : g_r.white.srv);
}
uint32_t drawn = 0;
@ -17794,16 +17860,60 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
g_refl_gate.store(gate, std::memory_order_relaxed);
}
}
// World-shading v2 (fams 1-4): the material's real per-pixel maps. The
// base normal map rides the free t5 pair slot (t4 holds the spec masks
// or the decal art there), the detail normal map + the decal families'
// spec/ecc masks ride the t8/t9 pair. misc.z carries the bind flags
// (1 = normal, 2 = detail, 4 = spec2), misc.w the detailNormalUVScale
// channel constant; both slots are spare on opaque fams 1-4 (fog only
// uses them on transparent/water items, F12 only on fams 5/6/13).
const GuestTexture* v2_detail = nullptr;
const GuestTexture* v2_spec2 = nullptr;
uint32_t v2_flags = 0;
if (item.env_family >= 1 && item.env_family <= 4 && !item.transparent &&
!item.water && debug_mode == 0 && scene.shadow_valid &&
REXCVAR_GET(skate3_native_render_scene_world_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; // t5 pair bind; overlay.w stays 0..3 here
v2_flags |= 1;
}
}
// Fam 2 (environmentsimple) carries a plain normal map with no
// base+detail pair; its vnd is the raw 2t-1 (shader-side).
if ((v2_flags & 1u) != 0 && item.env_family != 2 &&
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.decal && 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;
}
}
}
constants[44] = item.macro_scale;
constants[45] = item.macro_opacity;
constants[46] = macro_tex != &g_r.white ? 1.0f : 0.0f;
// overlay.w: 1 = single-placement decal (art clamps), 2 = tileable
// decal (art wraps; clamping a many-period uv range stretched the
// border texels into the giant cliff-face streaks), 3 = spec masks
// bound (exact env families).
constants[47] = is_decal ? (item.decal_tileable ? 2.0f : 1.0f)
: (spec_bound ? (normal_paired ? 4.0f : 3.0f)
: 0.0f);
// bound (exact env families). The 4-encode (t5 normal pair driving the
// reflective trim path) is fams 5/6 only; fams 1-4 signal their
// normal map through the misc.z flags instead.
constants[47] =
is_decal ? (item.decal_tileable ? 2.0f : 1.0f)
: (spec_bound ? ((normal_paired && item.env_family >= 5)
? 4.0f
: 3.0f)
: 0.0f);
if (item.water) {
// overlay.x = ripple scroll time, overlay.y = real environment cube
// bound at t6, overlay.z = ripple normal map resolved (in the macro
@ -17833,6 +17943,11 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
constants[50] = scene.fog_ramp[1];
constants[51] = scene.fog_ramp[2];
std::memcpy(constants + 40, scene.fog_color, 4 * sizeof(float));
} else if (v2_flags != 0) {
// misc.z = the v2 material bind flags, misc.w = detailNormalUVScale
// (fams 1-4 opaque; see the v2 block above).
constants[50] = float(v2_flags);
constants[51] = item.detail_scale;
} else if ((item.env_family >= 5 && item.env_family <= 6) ||
item.env_family == 13) {
// misc.y = cube LOD bias for the reflective families: the guest's
@ -17938,7 +18053,14 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
} else {
cmd->SetTexture(5, decal_tex->srv);
}
cmd->SetTexture(8, cube_tex->srv);
// t6 (cube) shares its table with t7 (shadow atlas); re-pair both.
cmd->SetTexturePair(7, cube_tex->srv,
shadow_ready ? g_r.shadow_srv_final : g_r.white.srv);
// World-shading v2 material pair (t8 detail + t9 decal spec).
if (v2_flags != 0) {
cmd->SetTexturePair(8, (v2_detail != nullptr ? v2_detail : &g_r.white)->srv,
(v2_spec2 != nullptr ? v2_spec2 : &g_r.white)->srv);
}
// ROPA shape blend (see RendererState::ropa_shapes): combine the shape
// generations with the kernel weights InterpolateDynamicItems computed
// (the SAME 8-tap boxcar / pair-lerp the body bones and garment world

View file

@ -204,8 +204,12 @@ struct DrawItem {
uint32_t spec_tex = 0;
// "detail" channel texture (env families 5/6): a constant 16x16 BC1 whose
// hardware-decoded value folds into the normal composition as 2*d - 1,
// derived per material at draw time (see the refl_bias cvars).
// derived per material at draw time (see the refl_bias cvars). On fams
// 1/3/4 (per-pixel world shading) it is a REAL tiling detail normal map,
// sampled per-pixel at t8 with the detailNormalUVScale channel constant
// below.
uint32_t detail_tex = 0;
float detail_scale = 0.0f;
float tint[4]; // rgb + enable flag in w
// The item's per-draw state (bone palette for skinned, world matrix for
// rigid model-space props) was not available at capture time; deferred