Reflective_trans glass (fam 13) + game-parity backface culling

This commit is contained in:
mchughalex 2026-07-11 06:18:35 +01:00
parent 5bba302100
commit 6497488c42
3 changed files with 217 additions and 28 deletions

View file

@ -15,6 +15,7 @@ REXCVAR_DECLARE(bool, skate3_native_render_scene_macro);
REXCVAR_DECLARE(bool, skate3_native_render_scene_decals); REXCVAR_DECLARE(bool, skate3_native_render_scene_decals);
REXCVAR_DECLARE(bool, skate3_native_render_scene_transparents); REXCVAR_DECLARE(bool, skate3_native_render_scene_transparents);
REXCVAR_DECLARE(bool, skate3_native_render_scene_shadows); REXCVAR_DECLARE(bool, skate3_native_render_scene_shadows);
REXCVAR_DECLARE(bool, skate3_native_render_scene_backface_cull);
REXCVAR_DECLARE(bool, skate3_native_render_scene_2d); REXCVAR_DECLARE(bool, skate3_native_render_scene_2d);
REXCVAR_DECLARE(bool, skate3_native_render_scene_splines); REXCVAR_DECLARE(bool, skate3_native_render_scene_splines);
REXCVAR_DECLARE(bool, skate3_native_render_scene_quadlists); REXCVAR_DECLARE(bool, skate3_native_render_scene_quadlists);
@ -138,8 +139,13 @@ void NativeDebugDialog::OnDraw(ImGuiIO& io) {
CvarCheckbox("Dynamic shadows", CvarCheckbox("Dynamic shadows",
REXCVAR_GET(skate3_native_render_scene_shadows), REXCVAR_GET(skate3_native_render_scene_shadows),
"Native CSM: skater/NPC/prop shadows onto the world")); "Native CSM: skater/NPC/prop shadows onto the world"));
REXCVAR_SET(skate3_native_render_scene_backface_cull,
CvarCheckbox("Backface cull (game parity)",
REXCVAR_GET(skate3_native_render_scene_backface_cull),
"World env materials cull FRONT like the game's material "
"XMLs; off = legacy cull-none (shows interior faces)"));
ImGui::SeparatorText("Reflective glass isolation (env fam 5/6)"); ImGui::SeparatorText("Reflective glass isolation (env fam 5/6/13)");
{ {
int mode = REXCVAR_GET(skate3_native_render_scene_refl_mode); int mode = REXCVAR_GET(skate3_native_render_scene_refl_mode);
const char* kReflModes[] = {"0: normal", const char* kReflModes[] = {"0: normal",
@ -148,8 +154,11 @@ void NativeDebugDialog::OnDraw(ImGuiIO& io) {
"3: flat normal (no normal map)", "3: flat normal (no normal map)",
"4: visualize cube sample only", "4: visualize cube sample only",
"5: body only (no spec, no cube)", "5: body only (no spec, no cube)",
"6: normal-map LOD bias (slider)"}; "6: normal-map LOD bias (slider)",
if (ImGui::Combo("refl mode", &mode, kReflModes, 7)) { "7: visualize lightmap sample",
"8: visualize lightmap UV (frac x16)",
"9: lightmap resolve status (blue = missing)"};
if (ImGui::Combo("refl mode", &mode, kReflModes, 10)) {
REXCVAR_SET(skate3_native_render_scene_refl_mode, mode); REXCVAR_SET(skate3_native_render_scene_refl_mode, mode);
} }
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {

View file

@ -216,6 +216,21 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_tex_revalidate, true, "Skate 3",
"Re-fingerprint cached texture payloads every 16 frames and " "Re-fingerprint cached texture payloads every 16 frames and "
"re-decode on change (heals late-composed lightmap pages)") "re-decode on change (heals late-composed lightmap pages)")
.lifecycle(rex::cvar::Lifecycle::kHotReload); .lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_lm_dump, false, "Skate 3",
"Diagnostic: dump the decoded mip 0 of every generated-mip "
"(no-chain composed page) texture to native_texture_dumps/ "
"as raw RGBA for offline diffing against the gsnap decode")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_backface_cull, true, "Skate 3",
"Backface-cull world env materials like the game does: the "
"material XMLs set CULLMODE=FRONT on every environment "
"family (banners calibrated game-kept faces = our D3D12 "
"BACK faces -> CULL_FRONT). CULL_NONE stacked hidden faces "
"into the frame: double glass panes + interior wall faces "
"behind the translucent canopy glass = the too-bright "
"slope / too-dark awning deltas. Trees/alphatest (fams "
"7/9/10) and mirrored instances stay uncull(ed).")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_mesh_revalidate, true, "Skate 3", REXCVAR_DEFINE_BOOL(skate3_native_render_scene_mesh_revalidate, true, "Skate 3",
"Re-decode cached meshes when their payload fingerprint changes " "Re-decode cached meshes when their payload fingerprint changes "
"(streaming arena reuse; also picks up CPU-animated buffers)") "(streaming arena reuse; also picks up CPU-animated buffers)")
@ -1833,6 +1848,12 @@ bool BuildItemFromMesh(uint8_t* base, uint32_t mesh, DrawItem& item) {
item.env_family = 3; item.env_family = 3;
} else if (is("environment.reflective_simple", 30)) { } else if (is("environment.reflective_simple", 30)) {
item.env_family = 6; item.env_family = 6;
} else if (is("environment.reflective_trans", 29)) {
// transparentenvironmentreflective: the sloped glass canopy /
// awning panels. Blended in the sorted alpha sub-pass with the
// fam-5 shading minus kd/macro, premultiplied body, out a^2
// (model verified 0.0-error against the ucode).
item.env_family = 13;
} else if (is("environment.reflective", 23)) { } else if (is("environment.reflective", 23)) {
item.env_family = 5; item.env_family = 5;
} else if (is("environmentsimple.alphatest", 28)) { } else if (is("environmentsimple.alphatest", 28)) {
@ -6587,15 +6608,40 @@ inline uint64_t FetchWordsKey(const uint32_t words[6]) {
return key; return key;
} }
// FNV-1a over 16 qwords sampled across a guest payload (SEH-guarded reads; // FNV-1a guest-payload fingerprint (SEH-guarded reads; streaming can
// streaming can decommit the range). Returns 0 only on unreadable payloads. // decommit the range). Returns 0 only on unreadable payloads.
// Payloads up to 64 KB hash FULLY: the runtime-composed lightmap atlas
// pages (32 KB DXT1) receive REGIONAL in-place writes as the game composes
// late-streamed cells into a page we already decoded; the old 16-qword
// probe missed writes that fell between its samples, so the revalidation
// pass never re-decoded and the affected cells served the half-composed
// first decode forever (the 2x-bright canopy / dark awning reflective_trans
// glass: one texture, early-composed cells correct, late cells
// stale). Larger payloads (streamed assets whose fetch words change when
// content moves) keep a strided sample, densified 16 -> 64 qwords.
uint64_t SamplePayloadFingerprint(uint8_t* base, uint32_t addr, uint32_t size) { uint64_t SamplePayloadFingerprint(uint8_t* base, uint32_t addr, uint32_t size) {
if (addr == 0 || size < 8) { if (addr == 0 || size < 8) {
return 0; return 0;
} }
uint64_t h = 1469598103934665603ull; uint64_t h = 1469598103934665603ull;
for (uint32_t k = 0; k < 16; ++k) { if (size <= 65536) {
const uint32_t off = uint32_t(uint64_t(size - 8) * k / 15u) & ~7u; uint64_t buf[512];
for (uint32_t off = 0; off < size; off += sizeof(buf)) {
const uint32_t n = std::min<uint32_t>(sizeof(buf), (size - off) & ~7u);
if (n == 0) {
break;
}
if (!GuestTryCopy(buf, base + addr + off, n)) {
return 0;
}
for (uint32_t k = 0; k < n / 8; ++k) {
h = (h ^ buf[k]) * 1099511628211ull;
}
}
return h;
}
for (uint32_t k = 0; k < 64; ++k) {
const uint32_t off = uint32_t(uint64_t(size - 8) * k / 63u) & ~7u;
uint64_t v = 0; uint64_t v = 0;
if (!GuestTryCopy(&v, base + addr + off, sizeof(v))) { if (!GuestTryCopy(&v, base + addr + off, sizeof(v))) {
return 0; return 0;
@ -7138,9 +7184,13 @@ float4 ps_main(VSOut i) : SV_Target {
// Exact env families (cam_pos.w < 0) use the game's world ALPHAREF 30. // Exact env families (cam_pos.w < 0) use the game's world ALPHAREF 30.
// dynamicobject items (cam_pos.w <= -21) are excluded here and clip in // dynamicobject items (cam_pos.w <= -21) are excluded here and clip in
// their own branch (only the .alphatest variant tests, at ALPHAREF 30). // their own branch (only the .alphatest variant tests, at ALPHAREF 30).
// Fam 13 (reflective_trans glass, cam_pos.w = -13) never alpha-tests;
// it alpha-BLENDS in the sorted sub-pass.
if (cam_pos.w > -12.5 || cam_pos.w < -13.5) {
float aref = cam_pos.w < -0.5 ? 0.1176 : 0.35; float aref = cam_pos.w < -0.5 ? 0.1176 : 0.35;
clip(misc.x > 0.0 ? albedo.a * albedo.a - 0.0627 : albedo.a - aref); clip(misc.x > 0.0 ? albedo.a * albedo.a - 0.0627 : albedo.a - aref);
} }
}
// Exact sky dome (cam_pos.w = -40; sky_defaultPS transcribed from the // Exact sky dome (cam_pos.w = -40; sky_defaultPS transcribed from the
// Skate 3 ucode in a live capture). The emulated frame's big sun // Skate 3 ucode in a live capture). The emulated frame's big sun
// glow is computed IN the dome shader: a 1D radial gradient (the sky // glow is computed IN the dome shader: a 1D radial gradient (the sky
@ -7360,7 +7410,7 @@ float4 ps_main(VSOut i) : SV_Target {
float3 lin; float3 lin;
float out_a = 1.0; float out_a = 1.0;
bool reduced_tone = false; bool reduced_tone = false;
if (fam > 8.5) { if (fam > 8.5 && fam < 12.5) {
// tree/treeanimate: D^2 * max(lm^2, floor) * scale [* tint mult]; // tree/treeanimate: D^2 * max(lm^2, floor) * scale [* tint mult];
// proxyworld/incandescent: D^2 * scale. No shadow receive, no kd, no // proxyworld/incandescent: D^2 * scale. No shadow receive, no kd, no
// material multiplier on the fog term. // material multiplier on the fog term.
@ -7380,7 +7430,8 @@ float4 ps_main(VSOut i) : SV_Target {
// against (CSM s + shadow color), kd, phong spec vs the shader's // against (CSM s + shadow color), kd, phong spec vs the shader's
// fixed literal light, cube reflection on 5/6. // fixed literal light, cube reflection on 5/6.
float3 ov = float3(1.0, 1.0, 1.0); float3 ov = float3(1.0, 1.0, 1.0);
if (overlay.z > 0.0) { // Fam 13 (transparentenvironmentreflective) carries no macro term.
if (overlay.z > 0.0 && fam < 12.5) {
float3 mo = macro.Sample(smp, i.uv * overlay.x).rgb; float3 mo = macro.Sample(smp, i.uv * overlay.x).rgb;
ov = saturate((mo - 0.5) * overlay.y + 0.5); ov = saturate((mo - 0.5) * overlay.y + 0.5);
} }
@ -7415,11 +7466,37 @@ float4 ps_main(VSOut i) : SV_Target {
s = saturate((sm2.x >= rd ? 1.0 : 0.0) + (1.0 - sm2.y)); s = saturate((sm2.x >= rd ? 1.0 : 0.0) + (1.0 - sm2.y));
} }
} }
float3 lmg = lightmap.Sample(smp, i.uv2).rgb; // Lightmap fetch = the console's semantics: BILINEAR, CLAMPED, mip 0.
// 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
// WRAP sampler over our generated mip chain averaged NEIGHBORING
// atlas cells together at grazing angles (deep aniso LOD), and at the
// page border the wrap pulled in the opposite edge of the page: the
// reflective_trans canopy slope (cells at v ~ 0.996, white-cliff
// cells wrapping in from v ~ 0) glowed ~2x bright while the right
// awning went dark, with the decode, UVs and constants all verified
// exact (diagnosed via the mode-7 raw-lightmap isolation view).
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) {
return float4(lmg, 1.0);
}
if (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) {
return float4(tint.r, 0.0, 1.0 - tint.r, 1.0);
}
float3 lml = min(lmg * lmg, s + sh_color.rgb); float3 lml = min(lmg * lmg, s + sh_color.rgb);
// GetTangentLight with the neutral (flat) normal map: // GetTangentLight with the neutral (flat) normal map:
// 0.39 * 2.39562 exactly. // 0.39 * 2.39562 exactly. Fam 13 has no kd term at all; its body is
lin = lml * 0.93429 * dcol; // 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) { if (overlay.w > 2.5) {
// spec/ecc/refmask at t4: phong vs the FIXED literal light // 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), // (-0.14, 0.5, 0.9), power 10 + 290*ecc, tint (2.1, 1.8, 1.5),
@ -7512,8 +7589,8 @@ float4 ps_main(VSOut i) : SV_Target {
if (abs(misc.z - 5.0) > 0.5) { if (abs(misc.z - 5.0) > 0.5) {
lin += ks * float3(2.1, 1.8, 1.5) * lml.g * masks.x; lin += ks * float3(2.1, 1.8, 1.5) * lml.g * masks.x;
} }
if (fam > 4.5 && fam < 6.5 && abs(misc.z - 5.0) > 0.5 && if (((fam > 4.5 && fam < 6.5) || fam > 12.5) &&
abs(misc.z - 1.0) > 0.5) { abs(misc.z - 5.0) > 0.5 && abs(misc.z - 1.0) > 0.5) {
// Cube reflection: reflect(E, wN) with xy negated (the source's // Cube reflection: reflect(E, wN) with xy negated (the source's
// ref_vec.xy *= -1), luminosity lerped toward 1 by // ref_vec.xy *= -1), luminosity lerped toward 1 by
// 0.3 * sat(4*refmask - 2.6), x refmask x reflection_scale 1.5. // 0.3 * sat(4*refmask - 2.6), x refmask x reflection_scale 1.5.
@ -7541,7 +7618,12 @@ float4 ps_main(VSOut i) : SV_Target {
} }
} }
} }
if (fam > 6.5) { 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,
// same convention as transparentenvironment).
out_a = albedo.a * albedo.a;
} else if (fam > 6.5) {
out_a = albedo.a; out_a = albedo.a;
reduced_tone = fam > 7.5; // environmentdiffuse's cheap tonemap reduced_tone = fam > 7.5; // environmentdiffuse's cheap tonemap
} }
@ -8243,7 +8325,9 @@ bool DecodeMesh(ID3D12Device* device, uint8_t* base, const DrawItem& item,
out[2] = float(iz) / 511.0f; out[2] = float(iz) / 511.0f;
}; };
float n3[3] = {0.0f, 0.0f, 0.0f}; float n3[3] = {0.0f, 0.0f, 0.0f};
if (item.env_family != 0 && item.env_family <= 6 && item.uv2_fmt == 26) { if (item.env_family != 0 &&
(item.env_family <= 6 || item.env_family == 13) &&
item.uv2_fmt == 26) {
// Exact world families: the REAL vertex normal is packed in the // Exact world families: the REAL vertex normal is packed in the
// lightmap-unwrap element (fmt 26 s16x4): zw = normal.xy (snorm), and // lightmap-unwrap element (fmt 26 s16x4): zw = normal.xy (snorm), and
// the unwrap xy SIGN bits carry the handedness; sign.y flips // the unwrap xy SIGN bits carry the handedness; sign.y flips
@ -8431,7 +8515,8 @@ bool DecodeMesh(ID3D12Device* device, uint8_t* base, const DrawItem& item,
// foliage regression). Those keep the strict fully-twinned rule. // foliage regression). Those keep the strict fully-twinned rule.
const bool partial_rule_ok = const bool partial_rule_ok =
!item.transparent && item.dynobj != 2 && item.env_family != 0 && !item.transparent && item.dynobj != 2 && item.env_family != 0 &&
item.env_family != 7 && item.env_family != 9 && item.env_family != 10; item.env_family != 7 && item.env_family != 9 &&
item.env_family != 10 && item.env_family != 13;
two_sided = twins * 10 >= tris.size() * 6 || two_sided = twins * 10 >= tris.size() * 6 ||
(partial_rule_ok && twins >= 12 && twins * 33 >= tris.size()); (partial_rule_ok && twins >= 12 && twins * 33 >= tris.size());
if (two_sided) { if (two_sided) {
@ -8594,6 +8679,22 @@ bool UploadGeneratedMips(const NativeGuestOutputRenderContext& context, uint8_t*
} }
} }
// Diagnostic dump: what the RUNTIME decoded, for an offline byte diff
// against gsnap_tex_decode of the same fetch words (the reflective_trans
// glass panels' wrong-brightness hunt: their composed lightmap pages take
// exactly this generated-mips path).
if (REXCVAR_GET(skate3_native_render_scene_lm_dump)) {
char path[260];
std::snprintf(path, sizeof(path),
"native_texture_dumps/gen_%08X_%ux%u_t%u.rgba",
info.memory.base_address, width, height,
info.is_tiled ? 1u : 0u);
if (FILE* f = std::fopen(path, "wb")) {
std::fwrite(rgba.data(), 1, rgba.size(), f);
std::fclose(f);
}
}
// Box-filtered chain down to 1x1. // Box-filtered chain down to 1x1.
std::vector<std::vector<uint8_t>> mips; std::vector<std::vector<uint8_t>> mips;
mips.emplace_back(std::move(rgba)); mips.emplace_back(std::move(rgba));
@ -8955,6 +9056,26 @@ bool EnsureGuestTextureFromWords(const NativeGuestOutputRenderContext& context,
} }
} }
} }
// Diagnostic dump (skate3_native_render_scene_lm_dump): no-chain textures
// through the PLAIN path (the >512px composed lightmap pages the
// generated-mips gate excludes): mip 0 as linear block rows, raw guest
// block format post-endian-swap, for offline decode + byte-diff against
// gsnap_tex_decode of the same fetch words.
if (mip_count == 1 && REXCVAR_GET(skate3_native_render_scene_lm_dump)) {
char path[260];
std::snprintf(path, sizeof(path),
"native_texture_dumps/plain_%08X_%ux%u_f%u_t%u.blk",
info.memory.base_address, width, height,
uint32_t(info.format), info.is_tiled ? 1u : 0u);
if (FILE* f = std::fopen(path, "wb")) {
const MipPlan& p0 = plans[0];
for (uint32_t by = 0; by < p0.rows; ++by) {
std::fwrite(mapping + p0.offset + size_t(by) * p0.pitch, 1,
size_t(p0.cols) * bytes_per_block, f);
}
std::fclose(f);
}
}
out.upload->Unmap(0, nullptr); out.upload->Unmap(0, nullptr);
if (g_tex_stage_out != nullptr) { if (g_tex_stage_out != nullptr) {
@ -10641,7 +10762,8 @@ void WarmItemResources(const NativeGuestOutputRenderContext& context, uint8_t* b
} }
// Environment cube (negative-cached like the draw path). // Environment cube (negative-cached like the draw path).
if ((item.water || item.char_family >= 6 || if ((item.water || item.char_family >= 6 ||
(item.env_family >= 5 && item.env_family <= 6)) && (item.env_family >= 5 && item.env_family <= 6) ||
item.env_family == 13) &&
item.water_env != 0 && !g_r.cube_textures.contains(item.water_env)) { item.water_env != 0 && !g_r.cube_textures.contains(item.water_env)) {
if (!within()) { if (!within()) {
++wc.deferred; ++wc.deferred;
@ -11879,14 +12001,34 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
// sub-pass under their own cull PSOs; never reset those here) // sub-pass under their own cull PSOs; never reset those here)
const bool hair_pass = item.char_family >= 4 && item.char_family <= 5 && const bool hair_pass = item.char_family >= 4 && item.char_family <= 5 &&
item.char_rows[14 * 4 + 1] > 0.0f; item.char_rows[14 * 4 + 1] > 0.0f;
// reflective_trans glass draws in the blended sub-pass whenever its
// exact branch is live (same gate as the sub-pass routing below); the
// opaque cull-PSO reset must not fire there.
const bool refl_trans_pass =
item.env_family == 13 && debug_mode == 0 && scene.shadow_valid;
if (use_depth && !item.transparent && !item.water && !hair_pass && if (use_depth && !item.transparent && !item.water && !hair_pass &&
g_r.pso_cullback != nullptr) { !refl_trans_pass && g_r.pso_cullback != nullptr) {
const float* w = item.world; const float* w = item.world;
const float det3 = w[0] * (w[5] * w[10] - w[6] * w[9]) - const float det3 = w[0] * (w[5] * w[10] - w[6] * w[9]) -
w[1] * (w[4] * w[10] - w[6] * w[8]) + w[1] * (w[4] * w[10] - w[6] * w[8]) +
w[2] * (w[4] * w[9] - w[5] * w[8]); w[2] * (w[4] * w[9] - w[5] * w[8]);
// Game-parity backface culling: every world environment material's
// XML sets CULLMODE=FRONT (== our CULL_FRONT; the banner work
// calibrated game-kept faces as our D3D12 BACK faces). CULL_NONE
// showed interior/away faces the game never renders, e.g. the
// building wall's inside face stacking behind the translucent canopy
// glass. Trees/alphatest (fams 7/9/10: leaf/fence cards read from
// both sides) and mirrored instances (flipped winding) stay
// uncull(ed), matching the two-sided-sheet rules.
const bool cull_family =
REXCVAR_GET(skate3_native_render_scene_backface_cull) &&
item.env_family != 0 && item.env_family != 7 &&
item.env_family != 9 && item.env_family != 10 &&
item.env_family != 13;
ID3D12PipelineState* want = ID3D12PipelineState* want =
(buffers.two_sided_sheet && det3 >= 0.0f) ? g_r.pso_cullback : g_r.pso; ((buffers.two_sided_sheet || cull_family) && det3 >= 0.0f)
? g_r.pso_cullback
: g_r.pso;
if (want != scene_pso_bound) { if (want != scene_pso_bound) {
list.D3DSetPipelineState(want); list.D3DSetPipelineState(want);
scene_pso_bound = want; scene_pso_bound = want;
@ -12128,7 +12270,8 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
// invisible. // invisible.
const GuestTexture* cube_tex = &g_r.white_cube; const GuestTexture* cube_tex = &g_r.white_cube;
if ((item.water || item.char_family >= 6 || if ((item.water || item.char_family >= 6 ||
(item.env_family >= 5 && item.env_family <= 6)) && (item.env_family >= 5 && item.env_family <= 6) ||
item.env_family == 13) &&
item.water_env != 0) { item.water_env != 0) {
auto cit = g_r.cube_textures.find(item.water_env); auto cit = g_r.cube_textures.find(item.water_env);
if (cit == g_r.cube_textures.end()) { if (cit == g_r.cube_textures.end()) {
@ -12286,7 +12429,8 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
constants[50] = scene.fog_ramp[1]; constants[50] = scene.fog_ramp[1];
constants[51] = scene.fog_ramp[2]; constants[51] = scene.fog_ramp[2];
std::memcpy(constants + 40, scene.fog_color, 4 * sizeof(float)); std::memcpy(constants + 40, scene.fog_color, 4 * sizeof(float));
} else if (item.env_family >= 5 && item.env_family <= 6) { } 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 // misc.y = cube LOD bias for the reflective families: the guest's
// cube fetch computes its gradient LOD at the game's own 1152x640 // cube fetch computes its gradient LOD at the game's own 1152x640
// render; at 4K our reflection-vector gradients are ~3.4x smaller // render; at 4K our reflection-vector gradients are ~3.4x smaller
@ -12303,7 +12447,9 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
// fixed-point packed (each mapped to 0..999 around 500; float-exact). // fixed-point packed (each mapped to 0..999 around 500; float-exact).
// Only when the EXACT branch will run (same gate as cam_pos.w = -fam) // Only when the EXACT branch will run (same gate as cam_pos.w = -fam)
// - the legacy fallback reads misc.x as the transparent/water flag. // - the legacy fallback reads misc.x as the transparent/water flag.
if (debug_mode == 0 && scene.shadow_valid) { // Fam 13 has no normal map (its PS reflects off the vertex normal),
// so its misc.x stays 0.
if (debug_mode == 0 && scene.shadow_valid && item.env_family != 13) {
double bx = REXCVAR_GET(skate3_native_render_scene_refl_bias_x); double bx = REXCVAR_GET(skate3_native_render_scene_refl_bias_x);
double by = REXCVAR_GET(skate3_native_render_scene_refl_bias_y); double by = REXCVAR_GET(skate3_native_render_scene_refl_bias_y);
if (REXCVAR_GET(skate3_native_render_scene_refl_bias_auto) && if (REXCVAR_GET(skate3_native_render_scene_refl_bias_auto) &&
@ -12466,7 +12612,14 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
const bool hair_blend = item.char_family >= 4 && item.char_family <= 5 && const bool hair_blend = item.char_family >= 4 && item.char_family <= 5 &&
char_capture_ok; char_capture_ok;
const bool glass_blend = item.char_family == 7 && char_capture_ok; const bool glass_blend = item.char_family == 7 && char_capture_ok;
if ((item.transparent || item.water || hair_blend || glass_blend) && // environment.reflective_trans (fam 13): blended glass canopies, joins
// the sorted alpha sub-pass whenever its exact branch is live (same
// shadow_valid gate as cam_pos.w = -fam; the legacy fallback renders it
// opaque exactly as before classification).
const bool refl_trans_blend =
item.env_family == 13 && scene.shadow_valid;
if ((item.transparent || item.water || hair_blend || glass_blend ||
refl_trans_blend) &&
debug_mode == 0) { debug_mode == 0) {
if (REXCVAR_GET(skate3_native_render_scene_transparents)) { if (REXCVAR_GET(skate3_native_render_scene_transparents)) {
transparent_items.push_back(&item); transparent_items.push_back(&item);
@ -12488,6 +12641,7 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
return view_dist2(*a) > view_dist2(*b); return view_dist2(*a) > view_dist2(*b);
}); });
list.D3DSetPipelineState(use_depth ? g_r.pso_transparent : g_r.pso_nodepth); list.D3DSetPipelineState(use_depth ? g_r.pso_transparent : g_r.pso_nodepth);
ID3D12PipelineState* blend_bound = use_depth ? g_r.pso_transparent : g_r.pso_nodepth;
for (const DrawItem* item : transparent_items) { for (const DrawItem* item : transparent_items) {
const bool hair = item->char_family >= 4 && item->char_family <= 5 && const bool hair = item->char_family >= 4 && item->char_family <= 5 &&
item->char_rows[14 * 4 + 1] > 0.0f; item->char_rows[14 * 4 + 1] > 0.0f;
@ -12499,9 +12653,34 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
draw_item(*item); draw_item(*item);
list.D3DSetPipelineState(g_r.pso_hair_b); list.D3DSetPipelineState(g_r.pso_hair_b);
draw_item(*item); draw_item(*item);
list.D3DSetPipelineState(g_r.pso_transparent); list.D3DSetPipelineState(blend_bound);
continue; continue;
} }
// reflective_trans glass culls like the game (its material family's
// XML: CULLMODE=FRONT): the canopy panels are double-glazed pane
// PAIRS a few cm apart; uncull(ed) we composited BOTH panes (an
// extra a^2 blend layer the emulated frame doesn't have).
// pso_hair_b IS the transparent state with CULL_FRONT.
if (item->env_family == 13 && use_depth && g_r.pso_hair_b != nullptr &&
REXCVAR_GET(skate3_native_render_scene_backface_cull)) {
const float* w = item->world;
const float det3 = w[0] * (w[5] * w[10] - w[6] * w[9]) -
w[1] * (w[4] * w[10] - w[6] * w[8]) +
w[2] * (w[4] * w[9] - w[5] * w[8]);
ID3D12PipelineState* want =
det3 >= 0.0f ? g_r.pso_hair_b
: (use_depth ? g_r.pso_transparent : g_r.pso_nodepth);
if (want != blend_bound) {
list.D3DSetPipelineState(want);
blend_bound = want;
}
draw_item(*item);
continue;
}
if (blend_bound != (use_depth ? g_r.pso_transparent : g_r.pso_nodepth)) {
blend_bound = use_depth ? g_r.pso_transparent : g_r.pso_nodepth;
list.D3DSetPipelineState(blend_bound);
}
draw_item(*item); draw_item(*item);
} }
} }

View file

@ -143,7 +143,8 @@ struct DrawItem {
// 5 = environment.reflective, 6 = environment.reflective_simple, // 5 = environment.reflective, 6 = environment.reflective_simple,
// 7 = environmentsimple.alphatest, 8 = environmentsimple.diffuse, // 7 = environmentsimple.alphatest, 8 = environmentsimple.diffuse,
// 9 = tree.default, 10 = animated.tree, 11 = proxyworld.default, // 9 = tree.default, 10 = animated.tree, 11 = proxyworld.default,
// 12 = incandescent.default. // 12 = incandescent.default, 13 = environment.reflective_trans
// (transparentenvironmentreflective: blended glass, alpha sub-pass).
uint8_t env_family = 0; uint8_t env_family = 0;
// dynamicobject.fx family (movable props: dispensers, dumpsters, benches, // dynamicobject.fx family (movable props: dispensers, dumpsters, benches,
// cans, ...). 0 = not a dynamic object, 1 = dynamicobject.default, // cans, ...). 0 = not a dynamic object, 1 = dynamicobject.default,