render: split the scene and HDR shader sources into shared headers

This commit is contained in:
mchughalex 2026-07-19 19:55:25 +01:00
parent 37cc3715a0
commit 38343207ff
11 changed files with 4880 additions and 4813 deletions

View file

@ -286,6 +286,10 @@ set(SKATE3_NATIVE_SHADER_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/hdr.hlsl ${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/hdr.hlsl
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/ssr.hlsl ${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/ssr.hlsl
) )
# Shared .hlsli headers, inlined into the embedded sources by
# EmbedShaders.cmake: the embed must rerun when any of them changes.
file(GLOB SKATE3_NATIVE_SHADER_INCLUDES
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/*.hlsli)
set(SKATE3_NATIVE_SHADER_HEADER set(SKATE3_NATIVE_SHADER_HEADER
${CMAKE_CURRENT_BINARY_DIR}/include/skate3_native_shaders.h) ${CMAKE_CURRENT_BINARY_DIR}/include/skate3_native_shaders.h)
add_custom_command( add_custom_command(
@ -295,6 +299,7 @@ add_custom_command(
-DOUT=${SKATE3_NATIVE_SHADER_HEADER} -DOUT=${SKATE3_NATIVE_SHADER_HEADER}
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/EmbedShaders.cmake -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/EmbedShaders.cmake
DEPENDS ${SKATE3_NATIVE_SHADER_SOURCES} DEPENDS ${SKATE3_NATIVE_SHADER_SOURCES}
${SKATE3_NATIVE_SHADER_INCLUDES}
${CMAKE_CURRENT_SOURCE_DIR}/cmake/EmbedShaders.cmake ${CMAKE_CURRENT_SOURCE_DIR}/cmake/EmbedShaders.cmake
COMMENT "Embedding native renderer shaders" COMMENT "Embedding native renderer shaders"
VERBATIM) VERBATIM)

View file

@ -38,6 +38,26 @@ string(APPEND content "#pragma once\n\n")
foreach(var file IN ZIP_LISTS SHADER_VARS SHADER_FILES) foreach(var file IN ZIP_LISTS SHADER_VARS SHADER_FILES)
file(READ "${SHADER_DIR}/${file}" src) file(READ "${SHADER_DIR}/${file}" src)
# Inline `#include "x.hlsli"` directives so every embedded shader stays a
# single self-contained string: the D3D12 backend hands the text to
# D3DCompile without an include handler, and the shader bytecode cache
# hashes the full text; pre-expanding means an include edit changes the
# embedded source and invalidates cached bytecode naturally. The loop
# rescans after each replacement, so nested includes expand too; the
# iteration guard catches cycles.
set(guard 0)
while(src MATCHES "#include \"([^\"]+)\"")
set(inc "${CMAKE_MATCH_1}")
if(NOT EXISTS "${SHADER_DIR}/${inc}")
message(FATAL_ERROR "${file}: include not found: ${inc}")
endif()
file(READ "${SHADER_DIR}/${inc}" inc_src)
string(REPLACE "#include \"${inc}\"" "${inc_src}" src "${src}")
math(EXPR guard "${guard} + 1")
if(guard GREATER 32)
message(FATAL_ERROR "${file}: include expansion did not converge (cycle?)")
endif()
endwhile()
string(FIND "${src}" ")__hlsl__" collision) string(FIND "${src}" ")__hlsl__" collision)
if(NOT collision EQUAL -1) if(NOT collision EQUAL -1)
message(FATAL_ERROR "${file} contains the raw-string delimiter )__hlsl__") message(FATAL_ERROR "${file} contains the raw-string delimiter )__hlsl__")

View file

@ -57,45 +57,10 @@ cbuffer C : register(b0) {
float4 vs2; // tonemap: xyz = sun direction in AO view space (toward the float4 vs2; // tonemap: xyz = sun direction in AO view space (toward the
// sun), w = haze density (1 / view unit) // sun), w = haze density (1 / view unit)
}; };
// Per-frame shadow constant slice (the scene pass's b1; see scene.hlsl for // Per-frame shadow / lighting constant slice (the scene pass's b1) -
// the authoritative layout); ps_vol_shafts samples sun visibility with the // ps_vol_shafts samples sun visibility with the exact receiver rows
// exact receiver math the materials use. Declared through the native static // the materials use.
// sun-shadow rows; the water/ocean material rows in between are padding. #include "scene_frame_cb.hlsli"
cbuffer S : register(b1) {
float4 sh_x; // light-space X row (xyz) + translation (w)
float4 sh_y; // light-space Y row
float4 sh_z; // depth row (height ramp)
float4 sh_c1; // cascade 1 scale.xy + offset.zw
float4 sh_c2; // cascade 2 scale.xy + offset.zw
float4 sh_color; // shadow color rgb + luma
float4 sh_misc; // x = depth bias, y = enable, zw = atlas dimensions
float4 sh_sun; // xyz = sun direction (toward the sun), w = exposure
float4 sh_env;
float4 sh_fogp;
float4 sh_fogc;
float4 dyn_sun;
float4 dyn_amb;
float4 dyn_misc; // y = static world-shadow floor
float4 sh_char;
float4 sh_v2;
float4 dyn_wsx; // static world-shadow rows: u = dot(wp4, wsx)*0.5+0.5,
float4 dyn_wsy; // v = dot(wp4, wsy)*-0.5+0.5,
float4 dyn_wsz; // ray depth = dot(wp4, wsz)
// Rows 19-33 carry water/ocean material state, not consumed here.
float4 s_water[15];
float4 sh_pcss; // contact-hardening knobs (surface receivers only)
float4 sh_pcss2;
// Native static sun-shadow map transform (authoritative layout and
// receiver math in scene.hlsl SampleStaticSun): world -> far-tile rows
// uc = dot(nsm_*.xyz, wp) + nsm_*.w, depth = dot(nsm_z.xyz, wp) + nsm_z.w.
// nsm_p.x = strength (0 = feature off / map not rendered this frame),
// nsm_p2.x = base receiver bias, nsm_p2.w = tile size in pixels.
float4 nsm_x;
float4 nsm_y;
float4 nsm_z;
float4 nsm_p;
float4 nsm_p2;
};
Texture2D<float4> tex0 : register(t0); // per-pass primary input Texture2D<float4> tex0 : register(t0); // per-pass primary input
#ifdef VOL_MSAA #ifdef VOL_MSAA
Texture2DMS<float> depth_ms : register(t0); // ps_vol_linearize MSAA depth Texture2DMS<float> depth_ms : register(t0); // ps_vol_linearize MSAA depth

View file

@ -34,96 +34,7 @@ cbuffer C : register(b0) {
}; };
// Per-frame dynamic-shadow (CSM) receiver constants, captured from the // Per-frame dynamic-shadow (CSM) receiver constants, captured from the
// game's world-material PIXEL banks. // game's world-material PIXEL banks.
cbuffer S : register(b1) { #include "scene_frame_cb.hlsli"
float4 sh_x; // light-space X row (xyz) + translation (w) [PS c0]
float4 sh_y; // light-space Y row [PS c3]
float4 sh_z; // depth row (height ramp) [PS c4]
float4 sh_c1; // cascade 1 scale.xy + offset.zw [PS c1]
float4 sh_c2; // cascade 2 scale.xy + offset.zw [PS c2]
float4 sh_color; // shadow color rgb [PS c8] + its luma in w
float4 sh_misc; // x = depth bias [PS c5.x], y = enable,
// zw = atlas dimensions (3*tile, tile)
// Exact world-shading frame rows (consumed by the env-family branch):
float4 sh_sun; // xyz = sun direction [PS c6], w = scene exposure [c10.x]
float4 sh_env; // x = material multiplier [PS c11.y], yz = tree lightmap
// scale/floor [tree PS c0.xy], w = tree tint mult [c4.y]
float4 sh_fogp; // xyz = global fog ramp scale/bias/exp [VS c5],
// w = proxyworld scale [proxy PS c3.y]
float4 sh_fogc; // fog color rgb + transmittance scale in w [VS c6]
// dynamicobject.fx frame-global lighting rows (see FrameScene::dynobj_rows).
float4 dyn_sun; // xyz = sun direction (PS c9), w = scene exposure (c13.x)
float4 dyn_amb; // xyz = flat ambient (c15.rgb), w = bounce scale (c15.w)
float4 dyn_misc; // x = material multiplier (c14.y),
// y = static world-shadow floor (c8.w)
// Character CSM receive biases (game char PS bank, finest cascade first)
// + enable in w: the exact 9-tap character shadow sampling
// (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;
// 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;
// flowingwateralpha m_params rows (captured per frame from the water PS
// bank; consumed by the exact water branch, cam_pos.w = -30):
float4 wat_p0; // m_params[0]: tangent-normal scale xzw, material mult y
float4 wat_p1; // m_params[1]: animated-UV scroll speeds (tap1 xy, tap2 zw)
float4 wat_p2; // m_params[2]: tap UV scales (tap1 xy, tap2 zw)
float4 wat_p3; // x = g_fAnimationTime, y = mask threshold,
// z = spec power, w = alpha floor
// ocean_defaultPS rows (exact ocean branch, cam_pos.w = -31): the PCA
// mean (pre-swizzled to R,G,B channel order) + the six weight rows
// (component-0/1 pairs for R, G, B) + m_params[0..2].
float4 oc_mean;
float4 oc_w0; // R, component 0
float4 oc_w1; // R, component 1
float4 oc_w2; // G, component 0
float4 oc_w3; // G, component 1
float4 oc_w4; // B, component 0
float4 oc_w5; // B, component 1
float4 oc_p0; // m_params[0]: ward spec color
float4 oc_p1; // m_params[1]: y = multiplier, z = tonedown scale,
// w = PCA-tap uv scale
float4 oc_p2; // m_params[2]: ward spread u/v (xy), roughness (z)
// oceanreflection_defaultPS row (fam 32): x = multiplier, y/z = the
// world-height alpha fade bounds.
float4 orf;
// Contact-hardening soft shadows (PCSS over the same cascade atlas; see
// SampleCsmShadowSoft): x = enable, y = tan of the sun's angular radius
// (penumbra width per meter of caster distance), z = max penumbra
// half-width in meters, w = static-caster receive bias in depth units
// (slope-scaled in the sampler; zero when static geometry is not in the
// atlas).
float4 sh_pcss;
// x = blocker-search radius in meters, y = meters per depth-map unit
// (1 / |sh_z.xyz|), z = light-space units per meter (|sh_x.xyz|, base
// cascade), w = minimum filter radius in physical atlas texels.
float4 sh_pcss2;
// Native static sun-shadow map transform (see SampleStaticSun): the
// world -> FAR-tile rows. uc = dot(nsm_x.xyz, wp) + nsm_x.w in [-1,1]
// (same for y), depth = dot(nsm_z.xyz, wp) + nsm_z.w in [0,1]. The near
// tile shares the origin, so its coordinate is exactly uc * nsm_p2.z.
float4 nsm_x;
float4 nsm_y;
float4 nsm_z;
// x = static shadow strength (0 = feature off), y = far-tile uc units
// per meter (1 / far radius), z = meters per depth unit, w = minimum
// filter radius in tile uc units.
float4 nsm_p;
// x = base receiver bias (depth units), y = slope-scaled bias term
// (depth units), z = reserved (cascade ratios are the shader-side
// kRatioInner/kRatioMid constants), w = tile size in pixels (tap
// clamping).
float4 nsm_p2;
};
// Character-family lighting (defaultcharacter.fx and friends): canonical // Character-family lighting (defaultcharacter.fx and friends): canonical
// per-draw rows captured from the guest PIXEL constant bank at palette // per-draw rows captured from the guest PIXEL constant bank at palette
// capture (CaptureCharLighting has the per-family register maps; the math // capture (CaptureCharLighting has the per-family register maps; the math
@ -224,415 +135,13 @@ VSOut vs_main(float3 p : POSITION, float2 uv : TEXCOORD0, float2 uv2 : TEXCOORD1
: float4(mul(sb, (float3x3)world), bw.w); : float4(mul(sb, (float3x3)world), bw.w);
return o; return o;
} }
// Native CSM atlas sample at a world position: finest covering cascade, #include "scene_shadows.hlsli"
// s = saturate(infront + 1 - coverage). Returns 1 (lit) when uncovered or #include "scene_common.hlsli"
// shadows are off. extra_bias suppresses receiver self-shadow acne on #include "scene_char.hlsli"
// surfaces that are themselves casters (characters, held board), in DEPTH #include "scene_dynobj.hlsli"
// UNITS like the game's own bias constants (receiver c5.x, the dynobj #include "scene_water.hlsli"
// literals); a world-metric bias was tried for the editor face band and // The full material shading for every family (ps_main forwards here).
// REVERTED: the band was the HAT casting (the game's shadow passes skip float4 ShadePixel(VSOut i) {
// it, see shadow_caster parity), not under-biasing, and the 0.144 m value
// erased the legit head-onto-neck shadow. A NEGATIVE extra_bias selects
// the game's dynamicobject receive bias instead: a per-cascade literal
// 0.007 (finest) / 0.015 (outer) replacing sh_misc.x
// (from the dynamicobject_defaultPS ucode). Props
// are casters themselves, and with only the world bias (c5.x = 0 in every
// capture) their flat tops compared against their own atlas depth; the
// lit/dark whole-surface flicker on benches, signs and sails as the
// camera-following cascades drifted frame to frame.
float SampleCsmShadow(float3 wp, float extra_bias) {
if (sh_misc.y <= 0.0) {
return 1.0;
}
float2 lsv = float2(dot(sh_x.xyz, wp) + sh_x.w, dot(sh_y.xyz, wp) + sh_y.w);
float2 luv = 0.0;
float casc = 0.0;
float2 l2 = lsv * sh_c2.xy + sh_c2.zw;
if (max(abs(l2.x), abs(l2.y)) < 0.99) { luv = l2; casc = 3.0; }
float2 l1 = lsv * sh_c1.xy + sh_c1.zw;
if (max(abs(l1.x), abs(l1.y)) < 0.99) { luv = l1; casc = 2.0; }
if (max(abs(lsv.x), abs(lsv.y)) < 0.99) { luv = lsv; casc = 1.0; }
if (casc <= 0.0) {
return 1.0;
}
float2 suv = float2(luv.x / 6.0 + (casc * 2.0 - 1.0) / 6.0,
luv.y * -0.5 + 0.5);
// Character receivers: the game's EXACT sampling (cacstamp_skin_nisPS
// ucode + live captured constants; offline-validated against the
// emulated frame). Reference = saturate(raw_depth - c9[cascade])
// (the per-cascade biases REPLACE the world receiver bias), NINE POINT
// taps at +-1 game-texel of the 512 tile (u step 1/1536 of the 3-tile
// atlas), binary sge each, averaged x 1/9 (literal c255.w). No coverage
// term: uncovered texels hold the far clear and compare lit.
if (extra_bias > 0.0 && sh_char.w > 0.0) {
float cbias = casc > 2.5 ? sh_char.z : (casc > 1.5 ? sh_char.y : sh_char.x);
float refd = saturate(dot(sh_z.xyz, wp) + sh_z.w - cbias);
// Tap offsets in game-map texels, BILINEAR-filtered like the game's
// fetches; the atlas blur dilates depth outward exactly so filtered
// compares stay valid across coverage edges; point-snapped taps
// quantized the penumbra into a stippled banding ("glisten"). Taps
// clamp half a texel inside the tile so filtering never bleeds the
// neighboring cascade.
float2 atlas_dim = float2(sh_misc.z, sh_misc.w);
float2 game_texel = float2(1.0 / 1536.0, 1.0 / 512.0);
float tile_x0 = (casc * 2.0 - 2.0) / 6.0;
float tile_x1 = casc * 2.0 / 6.0;
float acc = 0.0;
[unroll] for (int k = 0; k < 9; ++k) {
const float2 kOfs[9] = {float2(0.0, 0.0), float2(-1.0, -1.0),
float2(0.0, -1.0), float2(1.0, -1.0),
float2(-1.0, 0.0), float2(1.0, 0.0),
float2(-1.0, 1.0), float2(0.0, 1.0),
float2(1.0, 1.0)};
float2 s = suv + kOfs[k] * game_texel;
s.x = clamp(s.x, tile_x0 + 0.5 / atlas_dim.x, tile_x1 - 0.5 / atlas_dim.x);
s.y = clamp(s.y, 0.5 / atlas_dim.y, 1.0 - 0.5 / atlas_dim.y);
acc += shadow_atlas.Sample(smp_clamp, s).x >= refd ? 1.0 : 0.0;
}
return acc / 9.0;
}
float bias = extra_bias < 0.0 ? (casc > 1.5 ? 0.015 : 0.007)
: sh_misc.x + extra_bias;
float rd = dot(sh_z.xyz, wp) + sh_z.w - bias;
float2 sm2 = shadow_atlas.Sample(smp_clamp, suv);
return saturate((sm2.x >= rd ? 1.0 : 0.0) + (1.0 - sm2.y));
}
// Contact-hardening soft shadows (PCSS) over the same cascade atlas,
// enabled by sh_pcss.x. A blocker search estimates the average caster
// depth inside a world-space search radius; the penumbra half-width then
// follows the sun's angular size times the caster-to-receiver distance
// along the light axis (converted between world meters, depth-map units
// and light-space UV via the CPU-computed sh_pcss2 row scales), and a
// rotated poisson-disk PCF filters at that width; shadows are crisp at
// the contact point and soften with caster height, instead of the single
// fixed-width penumbra the blur chain bakes in. Tap semantics match the
// exact path (sat(infront + 1 - coverage); the blur pass dilates depth
// outward so filtered compares stay valid across coverage edges), and the
// per-class bias conventions of SampleCsmShadow carry over via extra_bias.
// The receiver normal drives an additional slope-scaled bias (sh_pcss.w)
// so large static casters, ground sheets, facades at grazing sun angles,
// never compare against their own atlas depth.
static const float2 kPcssTaps[12] = {
float2(-0.326212, -0.405805), float2(-0.840144, -0.073580),
float2(-0.695914, 0.457137), float2(-0.203345, 0.620716),
float2(0.962340, -0.194983), float2(0.473434, -0.480026),
float2(0.519456, 0.767022), float2(0.185461, -0.893124),
float2(0.507431, 0.064425), float2(0.896420, 0.412458),
float2(-0.321940, -0.932615), float2(-0.791559, -0.597705)};
float CsmSoftTap(float2 suv, float refd, float tile_x0, float tile_x1) {
// Taps clamp half a texel inside the tile so filtering never bleeds the
// neighboring cascade (same convention as the exact character taps).
suv.x = clamp(suv.x, tile_x0 + 0.5 / sh_misc.z, tile_x1 - 0.5 / sh_misc.z);
suv.y = clamp(suv.y, 0.5 / sh_misc.w, 1.0 - 0.5 / sh_misc.w);
float2 sm = shadow_atlas.Sample(smp_clamp, suv);
return saturate((sm.x >= refd ? 1.0 : 0.0) + (1.0 - sm.y));
}
// Poisson-rotation angle for the CSM atlas sampler: interleaved gradient
// noise of the SCREEN pixel: stable per pixel, smooth under motion. A
// world-position hash was rejected (sin(43758 * x) amplifies
// sub-millimeter float wobble of the reconstructed position into a fresh
// rotation every frame; penumbras boiled). The static sun-shadow sampler
// uses NO noise at all; its soft-compare Vogel kernel is smooth without
// it (verified offline), and every noise scheme read as speckle, stripes
// or grain there.
float PcssRotation(float2 px) {
return 6.2831853 *
frac(52.9829189 * frac(0.06711056 * px.x + 0.00583715 * px.y));
}
float SampleCsmShadowSoft(float3 wp, float extra_bias, float3 nrm,
float2 px) {
if (sh_pcss.x <= 0.5) {
return SampleCsmShadow(wp, extra_bias);
}
if (sh_misc.y <= 0.0) {
return 1.0;
}
float2 lsv = float2(dot(sh_x.xyz, wp) + sh_x.w, dot(sh_y.xyz, wp) + sh_y.w);
float2 luv = 0.0;
float casc = 0.0;
float2 cscale = float2(1.0, 1.0);
float2 l2 = lsv * sh_c2.xy + sh_c2.zw;
if (max(abs(l2.x), abs(l2.y)) < 0.99) { luv = l2; casc = 3.0; cscale = sh_c2.xy; }
float2 l1 = lsv * sh_c1.xy + sh_c1.zw;
if (max(abs(l1.x), abs(l1.y)) < 0.99) { luv = l1; casc = 2.0; cscale = sh_c1.xy; }
if (max(abs(lsv.x), abs(lsv.y)) < 0.99) { luv = lsv; casc = 1.0; cscale = float2(1.0, 1.0); }
if (casc <= 0.0) {
return 1.0;
}
// Receiver bias: the same per-class conventions as SampleCsmShadow
// (character per-cascade rows / dynobj literals / world bias), plus the
// slope-scaled static term.
float bias;
if (extra_bias > 0.0 && sh_char.w > 0.0) {
bias = casc > 2.5 ? sh_char.z : (casc > 1.5 ? sh_char.y : sh_char.x);
} else if (extra_bias < 0.0) {
bias = casc > 1.5 ? 0.015 : 0.007;
} else {
bias = sh_misc.x + extra_bias;
}
float ndl = 1.0;
if (dot(nrm, nrm) > 1e-4) {
ndl = abs(dot(normalize(nrm), sh_sun.xyz));
}
float slope = sqrt(saturate(1.0 - ndl * ndl)) / max(ndl, 0.12);
bias += sh_pcss.w * (1.0 + slope);
float refd = dot(sh_z.xyz, wp) + sh_z.w - bias;
float2 suv0 = float2(luv.x / 6.0 + (casc * 2.0 - 1.0) / 6.0,
luv.y * -0.5 + 0.5);
float tile_x0 = (casc * 2.0 - 2.0) / 6.0;
float tile_x1 = casc * 2.0 / 6.0;
// Light-space UV per meter for this cascade; luv -> atlas uv is
// (x / 6, y / 2) (three tiles across, [-1,1] vertical).
float2 uvpm = sh_pcss2.z * cscale;
float2 luv_to_suv = float2(1.0 / 6.0, 0.5);
float ang = PcssRotation(px);
float2 rot = float2(cos(ang), sin(ang));
// Blocker search over 7 taps in the search radius. The penumbra follows
// the NEAREST blocker (max depth below the reference), not the average:
// the search disk can straddle unrelated far casters; a receiver near a
// building's shadow region would average the building's depth into its
// own contact shadow and smear it to the max radius (the fully-blurred
// character shadow). The nearest blocker is the one whose penumbra the
// receiver is actually inside.
float2 rsearch = sh_pcss2.x * uvpm * luv_to_suv;
float bmax = -1.0;
{
float2 s = suv0;
s.x = clamp(s.x, tile_x0 + 0.5 / sh_misc.z, tile_x1 - 0.5 / sh_misc.z);
s.y = clamp(s.y, 0.5 / sh_misc.w, 1.0 - 0.5 / sh_misc.w);
float d = shadow_atlas.Sample(smp_clamp, s).x;
if (d < refd) { bmax = max(bmax, d); }
}
[unroll] for (int k = 0; k < 12; k += 2) {
float2 o = kPcssTaps[k];
o = float2(o.x * rot.x - o.y * rot.y, o.x * rot.y + o.y * rot.x);
float2 s = suv0 + o * rsearch;
s.x = clamp(s.x, tile_x0 + 0.5 / sh_misc.z, tile_x1 - 0.5 / sh_misc.z);
s.y = clamp(s.y, 0.5 / sh_misc.w, 1.0 - 0.5 / sh_misc.w);
float d = shadow_atlas.Sample(smp_clamp, s).x;
if (d < refd) { bmax = max(bmax, d); }
}
if (bmax < 0.0) {
// No caster inside the search footprint: every filter tap would
// compare lit (uncovered texels hold the far clear).
return 1.0;
}
float dist_m = max(refd - bmax, 0.0) * sh_pcss2.y;
float pen_m = min(dist_m * sh_pcss.y, sh_pcss.z);
// Filter radius: the penumbra in atlas units, floored at a fraction of a
// physical atlas texel so edges always stay filtered.
float2 rfilt = max(pen_m * uvpm, sh_pcss2.w * 2.0 / sh_misc.w) * luv_to_suv;
float acc = CsmSoftTap(suv0, refd, tile_x0, tile_x1);
[unroll] for (int j = 0; j < 12; ++j) {
float2 o = kPcssTaps[j];
o = float2(o.x * rot.x - o.y * rot.y, o.x * rot.y + o.y * rot.x);
acc += CsmSoftTap(suv0 + o * rfilt, refd, tile_x0, tile_x1);
}
return acc / 13.0;
}
// Native static sun-shadow receive (t10). The game's cascade transforms
// use a stylized near-vertical projection (fine for its own dynamic
// casters inside a ~12 m height window, ~40 degrees away from the
// material sun); tall static geometry needs a true sun-aligned
// projection, so static shade comes from this separate camera-centered
// ortho map instead of the CSM atlas. The term bottoms out at
// 1 - strength (nsm_p.x), keeping live static shade believable where the
// baked lighting's own sun disagrees, and fades out at the map edge so
// coverage never pops. Contact-hardening PCSS matching the atlas sampler
// (nearest-blocker search + rotated poisson PCF) when PCSS is enabled;
// a single compare otherwise. Uncovered texels hold the far clear = lit.
// Clamped tap into one tile of the three-tile static sun atlas (inner,
// mid, far left to right); taps never bleed across the tile seams.
float NsmTap(float2 suv, float refd, float tile_u0) {
float hx = 0.1666667 / nsm_p2.w; // half a texel in atlas u (3 tiles wide)
float hy = 0.5 / nsm_p2.w;
suv.x = clamp(suv.x, tile_u0 + hx, tile_u0 + 0.3333333 - hx);
suv.y = clamp(suv.y, hy, 1.0 - hy);
// Explicit LOD (the map is single-mip): the PCF loop below is dynamic
// and implicit-gradient sampling is not allowed in divergent flow.
return static_sun.SampleLevel(smp_clamp, suv, 0.0).x;
}
float SampleStaticSun(float3 wp, float3 nrm, float2 px) {
if (nsm_p.x <= 0.0) {
return 1.0;
}
float2 uc_far = float2(dot(nsm_x.xyz, wp) + nsm_x.w,
dot(nsm_y.xyz, wp) + nsm_y.w);
// Screen-pixel footprint in far-tile uc, BEFORE any divergent return
// (gradient op; the enclosing family branches are uniform per draw).
// Scales the filter floor with viewing distance so far shadows
// integrate over the map's texel grid instead of aliasing against it:
// the shadow-map analogue of texture mip filtering. Close-ups keep the
// 2-texel floor untouched.
float pxfoot = max(fwidth(uc_far.x), fwidth(uc_far.y));
float edge = max(abs(uc_far.x), abs(uc_far.y));
if (edge >= 0.99) {
return 1.0;
}
// Fade at the outer (far-tile) boundary only; the near->far handoff
// needs none (the far tile covers the near region too).
float fade = saturate((0.97 - edge) / 0.07);
// Cascade select (3 tiles sharing the origin, so each finer coordinate
// is an exact rescale of the far one). Ratios 6/2/1: the inner cascade
// trades peak density for reach; foliage dapple at 16x density read as
// over-detailed and only engaged very close; 6x at a third of the far
// radius is the calibrated compromise. Integer 1:2:6 chain keeps every
// tile on the far-texel snap grid. Keep in sync with
// RenderStaticSunMap's kRatioInner/kRatioMid.
const float kRatioInner = 6.0;
const float kRatioMid = 2.0;
float2 uc0 = uc_far * kRatioInner;
float2 uc1 = uc_far * kRatioMid;
float tile;
float2 uc;
float scale;
if (max(abs(uc0.x), abs(uc0.y)) < 0.99) {
tile = 0.0;
uc = uc0;
scale = kRatioInner;
} else if (max(abs(uc1.x), abs(uc1.y)) < 0.99) {
tile = 1.0;
uc = uc1;
scale = kRatioMid;
} else {
tile = 2.0;
uc = uc_far;
scale = 1.0;
}
float tile_u0 = tile / 3.0;
float ucpm = nsm_p.y * scale; // uc units per meter in this tile
float2 suv = float2(uc.x / 6.0 + (tile * 2.0 + 1.0) / 6.0,
uc.y * -0.5 + 0.5);
const float2 uc_to_suv = float2(1.0 / 6.0, 0.5);
float ndl = 1.0;
if (dot(nrm, nrm) > 1e-4) {
ndl = abs(dot(normalize(nrm), sh_sun.xyz));
}
float slope = sqrt(saturate(1.0 - ndl * ndl)) / max(ndl, 0.12);
float refd = dot(nsm_z.xyz, wp) + nsm_z.w - (nsm_p2.x + nsm_p2.y * slope);
if (sh_pcss.x <= 0.5) {
float s0 = NsmTap(suv, refd, tile_u0) >= refd ? 1.0 : 0.0;
return 1.0 - fade * nsm_p.x * (1.0 - s0);
}
// FIXED kernels throughout this sampler: no per-pixel noise. Verified
// offline against synthetic roof/wall/streetlamp depth maps at the real
// map scales: every noise scheme (world hash, interleaved gradient
// noise, white hash, radius jitter) renders the penumbra as speckle,
// stripes or grain, while fixed taps with the soft compares below give
// a clean smooth gradient (the penumbra fraction varies smoothly across
// receivers, and bilinear map sampling fills between taps). Denser
// blocker searches were also tried offline and REJECTED; unstable
// nearest-blocker picks around small casters draw dark veins.
float2 rsearch = sh_pcss2.x * ucpm * uc_to_suv;
float bmax = -1.0;
{
float d = NsmTap(suv, refd, tile_u0);
if (d < refd) { bmax = d; }
}
[unroll] for (int k = 0; k < 12; k += 3) {
float d = NsmTap(suv + kPcssTaps[k] * rsearch, refd, tile_u0);
if (d < refd) { bmax = max(bmax, d); }
}
if (bmax < 0.0) {
return 1.0;
}
float dist_m = max(refd - bmax, 0.0) * nsm_p.z;
float pen_m = min(dist_m * sh_pcss.y, sh_pcss.z);
// Filter floor: 2 physical texels, raised to ~2 screen-pixel footprints
// at distance (lab-calibrated distance anti-aliasing).
float floor_uc = max(nsm_p.w, 2.0 * pxfoot * scale);
float r_uc = max(pen_m * ucpm, floor_uc);
float2 rfilt = r_uc * uc_to_suv;
// Vogel-spiral PCF with SOFT depth compares: binary taps quantize the
// penumbra into N discrete levels and the stable per-pixel rotation
// arranges the quantization into a visible cross-hatch ("fine mesh")
// pattern; a continuous per-tap contribution over a small depth window
// turns the same taps into a smooth gradient. The window rides the
// receiver bias so contact shadows stay grounded. Wide penumbras (tall
// roof edges cap the radius) undersample the base kernel and re-weave
// the mesh: they spend a second Vogel ring instead; the branch is
// spatially coherent across a penumbra.
float w = max(nsm_p2.x * 2.0, 1e-5);
// Tap count follows the FINAL radius (penumbra or adaptive floor,
// whichever won); wide kernels undersample at 13 taps.
int n = r_uc > 0.3 * ucpm ? 26 : 13;
float acc = 0.0;
[loop] for (int j = 0; j < n; ++j) {
float r = sqrt((float(j) + 0.5) / float(n));
float a = float(j) * 2.399963;
float2 o = float2(cos(a), sin(a)) * r;
float d = NsmTap(suv + o * rfilt, refd, tile_u0);
acc += saturate((d - refd) / w + 0.5);
}
float s = acc / float(n);
return 1.0 - fade * nsm_p.x * (1.0 - s);
}
// Screen-space UV-gradient tangent frame (cotangent reconstruction from the
// 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);
}
}
// ---- Output encode --------------------------------------------------------
// Every exact branch ends in the same chain: x = the post-fog,
// post-exposure linear value ("xe"), then
// tonemap = max(x*0.25 + 0.75, 1) - sat(1 - x)^2
// out = sat(sqrt(0.5 * tonemap) * 1.41) (the uber's 1.41 fold)
// Under HDR=1 the branch returns xe itself into the float scene
// intermediate and the host post pass (hdr.hlsl ps_tonemap) applies the
// chain exactly once, which is what lets real bloom inject energy
// pre-tonemap. `reduced` = environmentdiffuse's cheap curve 1 - sat(1-x)^2;
// it equals the full curve evaluated at min(x, 1) (the two only differ past
// the x = 1 saturation point), so the HDR encode is a clamp.
float4 ToneOut(float3 xe, float a, bool reduced) {
#ifdef HDR
float3 x = max(xe, 0.0);
return float4(reduced ? min(x, 1.0) : x, a);
#else
float3 t1 = saturate(1.0 - xe);
float3 tm = reduced ? 1.0 - t1 * t1 : max(xe * 0.25 + 0.75, 1.0) - t1 * t1;
return float4(saturate(sqrt(max(tm * 0.5, 0.0)) * 1.41), a);
#endif
}
// Passthrough for values authored in the FINAL gamma space (the legacy
// empirical branches, water, debug visualizations): under HDR they encode
// as the tone chain's exact inverse, so the host tonemap restores them.
float3 PassGamma(float3 c) {
#ifdef HDR
float3 tm = c * c * (2.0 / (1.41 * 1.41));
float3 lo = 1.0 - sqrt(saturate(1.0 - tm));
float3 hi = 4.0 * tm - 3.0;
return lerp(lo, hi, step(1.0, tm)); // per-channel select (fxc + dxc)
#else
return c;
#endif
}
float4 ps_main(VSOut i) : SV_Target {
if (tint.a > 0.0) { if (tint.a > 0.0) {
return float4(PassGamma(tint.rgb), tint.a); return float4(PassGamma(tint.rgb), tint.a);
} }
@ -682,275 +191,15 @@ float4 ps_main(VSOut i) : SV_Target {
float3 xe = lin * overlay.y * misc.y; float3 xe = lin * overlay.y * misc.y;
return ToneOut(xe, 1.0, false); return ToneOut(xe, 1.0, false);
} }
// dynamicobject.fx props (cam_pos.w = -(20 + variant): -21 default, // dynamicobject.fx props: scene_dynobj.hlsli. Variants -21/-22 only;
// -22 alphatest). Rigid movable objects (dispensers, dumpsters, benches, // the exact water families (-30..) and the sky (-40) pass through.
// cans). Lit with the game's own dynamicobject model (verified exact if (cam_pos.w < -20.5 && cam_pos.w > -25.5) {
// against an offline ucode evaluation): key sun light + bounce + return ShadeDynObject(i, albedo);
// flat ambient, gated by min(CSM, max(static world shade, c8.w floor)),
// then fog -> exposure -> tonemap -> sqrt and the postfx uber 1.41.
// v2: misc.z
// carries the material bind flags (1 = base normal at t5, 2 = detail at
// 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 && cam_pos.w > -25.5) { // dynobj variants only:
// -21/-22; the exact water family (-30) and sky (-40) pass through.
if (cam_pos.w < -21.5) {
clip(albedo.a - 0.1176); // dynamicobject.alphatest: ALPHAREF 30
}
float3 dlin = albedo.rgb * albedo.rgb;
float3 n = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
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 bounce = saturate(dot(wn, float3(-dyn_sun.x, dyn_sun.y, -dyn_sun.z)));
// CSM shadow (shared receiver rows at t7). extra_bias -1 = the game's
// own per-cascade dynamicobject receive bias (props are casters;
// without it their flat tops self-shadow and flicker).
float s = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, -1.0, wn, i.pos.xy),
SampleStaticSun(i.rpos + cam_pos.xyz, wn, i.pos.xy));
// Static world-shadow term: the game's baked building/tree shade,
// 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 lin = lighting * kd * 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.
float fdist = length(i.rpos);
float f1 = saturate(fdist * sh_fogp.x + sh_fogp.y);
if (sh_fogp.z != 1.0) {
f1 = pow(max(f1, 1e-6), sh_fogp.z);
}
float3 fog_rgb = sh_fogc.rgb * f1;
float fog_a = (1.0 + sh_fogc.a * f1) * dyn_misc.x; // x material multiplier
float3 xe = (lin * fog_a + fog_rgb) * dyn_sun.w;
return ToneOut(xe, 1.0, false);
} }
// Character families: the game's own lighting in LINEAR space (diffuse is // Character families (defaultcharacter / CAC / livingworld / hair /
// gamma -> square it), then the exact tone chain from the disassembly and // vehicles): scene_char.hlsli.
// the postfx uber's 1.41 scene multiplier (which the empirical world
// shading already folds into its constants; without it characters sit
// ~30% darker than their surroundings, measured on an F11 A/B pair).
if (cam_pos.w > 0.5) { if (cam_pos.w > 0.5) {
float fam = cam_pos.w; return ShadeCharacter(i, albedo);
float3 dlin = albedo.rgb * albedo.rgb;
float3 cn = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
float ndl = saturate(dot(cn, ch_light.xyz));
float3 vd = -normalize(i.rpos);
float3 lin;
float out_a = 1.0;
if (fam > 5.5) {
// Traffic vehicles (vehicle.fx fam 6 body / vehicle_glass.fx fam 7
// windows, disassembled from vehicle_defaultPS): paint recolor where
// the diffuse green channel is below the mask threshold (red-channel
// mask * colorize_red + blue-channel mask * colorize_blue, the taxi
// yellow), key light + the livingworld flat ambient, phong specular
// along the reflected sun, and an environment-cube reflection scaled
// by fresnel and the gloss packed in the diffuse alpha. Glass keeps
// only the reflection terms (its tint rows are zero) and blends at
// the captured alpha. overlay.y > 0 = the material's cube resolved
// at t6 (same convention as water).
float3 sel;
if (fam < 6.5) {
sel = dlin.g > 0.001225
? dlin
: ch_tintA.rgb * dlin.r + ch_tintB.rgb * dlin.b;
} else {
sel = ch_tintA.rgb;
}
// DXN panel normal map (the material's `normal` channel, riding the
// macro slot; overlay.z > 0 = resolved). The vertex layout carries no
// tangent frame, so build a screen-space cotangent frame from the
// position/uv derivatives; it reproduces the authored panel shading
// including mirrored UV islands. Skipping the map entirely shades the
// hinged panels by their vertex normals, which face away from the sun
// - the dark ambient-blue "misdrawn shadow" that stopped at the door
// seam (verified against the ucode: flat map = the artifact, real
// map = the emulated car).
float3 vn = cn;
if (overlay.z > 0.5) {
float2 nm = macro.Sample(smp, i.uv).rg * 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, cn), dp1p = cross(cn, dp1);
float3 tt = dp2p * du1.x + dp1p * du2.x;
float3 bb = dp2p * du1.y + dp1p * du2.y;
float im = rsqrt(max(max(dot(tt, tt), dot(bb, bb)), 1e-12));
vn = normalize(nm.x * tt * im + nm.y * bb * im +
cn * sqrt(saturate(1.0 - dot(nm, nm))));
}
float vndl = saturate(dot(vn, ch_light.xyz));
float3 rfl = ch_light.xyz - 2.0 * dot(vn, ch_light.xyz) * vn;
// The sun spec is gated on N.L >= 0 (the ucode multiplies the spec
// term by an sge result); the cube reflection is not.
float spec = pow(saturate(dot(vd, -rfl)), max(ch_sh[0].w, 1.0)) *
(dot(vn, ch_light.xyz) >= 0.0 ? 1.0 : 0.0);
float fres = pow(1.0 - saturate(dot(vn, vd)), max(ch_light.w, 1.0));
float3 cube = float3(0.0, 0.0, 0.0);
if (overlay.y > 0.5) {
cube = env_cube.Sample(smp, reflect(-vd, vn)).rgb;
cube *= cube; // the PS consumes the cube squared (linear space)
}
// Gloss = the diffuse alpha SQUARED: the ucode squares the whole
// diffuse fetch (linear-space decode), alpha included; raw alpha
// over-specs ~5x and mottles the body panels.
float gloss = fam < 6.5 ? albedo.a * albedo.a : 1.0;
lin = sel * (ch_key.rgb * vndl + ch_amb.rgb) +
(spec * ch_sh[0].rgb + cube) * fres * gloss;
out_a = ch_misc.x;
} else if (fam > 3.5) {
// Hair (cac_hair / defaulthair): key on a wrapped N.L ramp + flat
// ambient, fresnel rim tint on a steeper ramp; strand coverage from
// the mesh's "alpha" channel at the raw second texcoord (bound at t4)
// - alpha-blended in the sorted sub-pass (hair drawn opaque is the
// blocky-helmet look).
float fres = pow(1.0 - saturate(dot(cn, vd)), max(ch_light.w, 1.0));
float3 hl = ch_key.rgb * (saturate(ndl * 0.75 + 0.25) + ch_amb.w) +
ch_tintB.rgb * fres * saturate(ndl * 1.75 + 0.25);
lin = dlin * hl;
out_a = saturate(decal_art.Sample(smp, i.uv2).r * ch_tintB.w);
} else if (fam > 2.5) {
// livingworld pedestrians: the diffuse is a stamp-mask atlas: red
// regions recolor with tintA, blue with tintB (judged in linear
// space; real-color regions have green above the threshold).
// The game's character PSes multiply the key light by the CSM shadow
// (tap >= ray = lit); characters are casters themselves, so an extra
// receiver bias suppresses self-shadow acne while the body-onto-board
// / body-onto-NPC shading survives (the sun-axis depth gap there is
// tens of cm). Without this the held skateboard, a big flat surface
// that is almost always inside the skater's own shadow, renders
// fully sunlit (near-white) against the emulated dark deck.
float csm = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, 0.012, cn, i.pos.xy),
SampleStaticSun(i.rpos + cam_pos.xyz, cn, i.pos.xy));
float3 sel = dlin.g > 0.001225
? dlin
: ch_tintA.rgb * dlin.r + ch_tintB.rgb * dlin.b;
lin = sel * (ch_key.rgb * ndl * csm + ch_amb.rgb);
// livingworld_stamp_defaultPS ends `max oC0.w, c21.x`: the entity's
// spawn/distance fade. Only visible when the item is routed to the
// blended sub-pass (alpha < 1); the opaque pass ignores it.
out_a = ch_misc.x;
} else {
// defaultcharacter / CAC pieces: key light + SH irradiance ambient,
// key gated by the CSM shadow (see the livingworld comment above).
float csm = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, 0.012, cn, i.pos.xy),
SampleStaticSun(i.rpos + cam_pos.xyz, cn, i.pos.xy));
if (ch_tintA.w > 0.0) {
dlin *= ch_tintA.rgb;
}
float3 irr = saturate(
ch_sh[0].rgb + cn.x * ch_sh[1].rgb + cn.y * ch_sh[2].rgb +
cn.z * ch_sh[3].rgb + (cn.x * cn.z) * ch_sh[4].rgb +
(cn.z * cn.y) * ch_sh[5].rgb + (cn.y * cn.x) * ch_sh[6].rgb +
(cn.z * cn.z) * ch_sh[7].rgb +
(cn.x * cn.x - cn.y * cn.y) * ch_sh[8].rgb);
lin = dlin * (ch_key.rgb * ndl * csm + irr * ch_amb.w);
// defaultcharacter/cacstamp PSes end `max oC0.w, c13.x / c22.x`:
// the entity's spawn fade (see the livingworld comment above).
out_a = ch_misc.x;
// character.alpha accessory (sunglass lens): coverage from the mesh's
// "alpha" channel at the raw second texcoord, like hair (cac_alphaPS:
// oC0.w = tf5(uv2).r * c22.x), routed to the blended sub-pass.
if (ch_misc.z > 0.5) {
out_a *= saturate(decal_art.Sample(smp, i.uv2).r);
}
}
// Exact tone chain: sqrt(0.5 * (max(x*E/4 + 0.75, 1) - sat(1 - x*E)^2)).
float E = max(ch_key.w, 0.01);
return ToneOut(lin * E, out_a, false);
} }
// Exact world-material families (cam_pos.w = -family): hand-ported from // Exact world-material families (cam_pos.w = -family): hand-ported from
// the game's own pixel shaders and verified per-pixel against them with // the game's own pixel shaders and verified per-pixel against them with
@ -978,143 +227,16 @@ 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;
// flowingwateralpha (fam 30): the canal/waterfall water shader, // Water families (30 flowingwateralpha / 31 ocean /
// hand-ported from flowingwateralpha_defaultPS and verified per-pixel // 32 oceanreflection): exact ports in scene_water.hlsli.
// against the ucode (dual time-scrolled ripple-normal taps -> TBN ->
// lightmap-lit body + phong sun spec + cube reflection, then the
// standard fog/exposure/tonemap chain; alpha = the spec term's green
// channel, floored; reflection strength drives opacity over the bed).
if (fam > 29.5 && fam < 30.5) { if (fam > 29.5 && fam < 30.5) {
float t = wat_p3.x; return ShadeFlowingWater(i, dlin, fog_a, fog_rgb, expo);
float3 n1 = macro.Sample(smp, i.uv * wat_p2.xy + wat_p1.xy * t).rgb;
float3 n2 = macro.Sample(smp, i.uv * wat_p2.zw + wat_p1.zw * t).rgb;
float3 nsum = 2.0 * n1 + 2.0 * n2 - 2.0;
// vNormal = normalize(m_params[0].xzw * (n1 + n2)) in tangent space.
float3 vn = normalize(float3(nsum.x * wat_p0.x, nsum.y * wat_p0.z,
nsum.z * wat_p0.w));
float3 wn0 = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
// Stored frame: tanb = the mesh's usage-6 TANGENT here (see the water
// pack in DecodeMesh), sentinel sign = the unwrap sign.x;
// B = sign.x * cross(N, T), exactly the game's VS.
float3 wt, wb;
if (i.tanb.w > 0.1) {
wt = normalize(i.tanb.xyz);
wb = cross(wn0, wt) * (i.tanb.w > 0.6 ? 1.0 : -1.0);
} else {
ScreenTangentFrame(wn0, i.rpos, i.uv, wt, wb);
}
float3 wn = normalize(vn.x * wt + vn.y * wb + vn.z * wn0);
float3 vd = -normalize(i.rpos);
float s = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, 0.0, wn, i.pos.xy),
SampleStaticSun(i.rpos + cam_pos.xyz, wn, i.pos.xy));
// Lightmap at the unwrap + 0.01 * vNormal.xz (the ripple refraction
// wobble), console fetch semantics, min-clamped like every world
// family. White-fallback pages skip the clamp (see the env fetch).
float2 wlm_uv = i.uv2 + 0.01 * vn.xz;
float3 wlm = lightmap.SampleLevel(smp_clamp, wlm_uv, 0.0).rgb;
float3 lml = tint.r > 0.0 ? min(wlm * wlm, s + sh_color.rgb)
: wlm * wlm;
// GetTangentLight on the WORLD normal: fixed axis (0.58, 0.62, 0.39)
// with xy signs following the sun direction, x 1/normalize(axis).z.
float kd = (wn.x * 0.58 * sign(sh_sun.x) +
wn.y * 0.62 * sign(sh_sun.y) + wn.z * 0.39) * 2.39562;
lin = lml * kd * dlin;
// Spec/reflection masks (t4): x = phong mask, z = reflection mask,
// thresholded by m_params[3].y. Zero until resolved: matte water.
float2 wmask = float2(0.0, 0.0);
if (overlay.x > 0.0) {
float4 wm4 = decal_art.Sample(smp, i.uv);
wmask = saturate(float2(wm4.x, wm4.z) - wat_p3.y);
}
// Phong vs the real sun, power m_params[3].z, tint (2.1, 1.8, 1.5),
// scaled by sat(clamped lightmap green - 0.1).
float3 wrl = 2.0 * wn * dot(wn, sh_sun.xyz) - sh_sun.xyz;
float ks = pow(max(saturate(dot(vd, wrl)), 1e-6), wat_p3.z);
float3 wspec =
ks * wmask.x * float3(2.1, 1.8, 1.5) * saturate(lml.g - 0.1);
// Cube reflection, same shape as fams 5/6: reflect(E, wN) with xy
// negated, luminosity lerped toward 1 by 0.3 * sat(4*mask - 2.6),
// x mask x 1.5. misc.y = the cube LOD bias (640p-gradient parity).
float3 wrv = vd - 2.0 * wn * dot(vd, wn);
float3 wcube =
overlay.y > 0.0
? env_cube.SampleBias(smp, float3(-wrv.x, -wrv.y, wrv.z),
misc.y).rgb
: float3(0.0, 0.0, 0.0);
float wrlum = 0.3 * saturate(4.0 * wmask.y - 2.6);
float3 wrefl = wcube * (lml.g + wrlum * (1.0 - lml.g)) * wmask.y * 1.5;
float3 wspec_term = wspec + wrefl;
lin += wspec_term;
float3 wxe = (lin * (fog_a * wat_p0.y) + fog_rgb) * expo;
return ToneOut(wxe, max(wspec_term.g, wat_p3.w), false);
} }
// Exact ocean surface (fam 31, ocean_defaultPS; verified per-pixel
// against the ucode): PCA-compressed dual-component normal map, lerped
// toward flat by the overlay-scaled tonedown, cube reflection x squared
// lightmap x fresnel, ward anisotropic sun spec. OPAQUE (the game's PS
// outputs alpha 0 and draws without blending).
if (fam > 30.5 && fam < 31.5) { if (fam > 30.5 && fam < 31.5) {
float2 suv = i.uv * oc_p1.w; return ShadeOcean(i, fog_a, fog_rgb, expo);
float4 c0v = macro.Sample(smp, suv) * 2.0 - 1.0; // t3: comp 0
float4 c1v = detail_map.Sample(smp, suv) * 2.0 - 1.0; // t8: comp 1
// PCA normal in the shader's Z-UP basis; mean/weights are stored in
// 0..1 texture space and folded 2x-1 after the sum.
float3 pca = (float3(dot(c0v, oc_w0) + dot(c1v, oc_w1),
dot(c0v, oc_w2) + dot(c1v, oc_w3),
dot(c0v, oc_w4) + dot(c1v, oc_w5)) +
oc_mean.xyz) * 2.0 - 1.0;
// tonedown = 2 * m_params[1].z * sat(dot(c1v, wB1) + overlay.x):
// the ucode folds the second PCA blue dot into the overlay term.
float ovx = overlay.z > 0.0
? spec2_map.Sample(smp, i.uv * overlay.w).x
: 1.0;
float otd = (2.0 * oc_p1.z) * saturate(dot(c1v, oc_w5) + ovx);
float3 nt = normalize(lerp(float3(0.0, 0.0, 1.0), pca, otd));
nt = normalize(max(abs(nt), 0.001));
float3 N = float3(nt.x, nt.z, nt.y); // shader Z-up -> world Y-up
float3 E = -normalize(i.rpos);
// Cube reflection: reflect(E, N) with xy negated, x the SQUARED
// single-tap lightmap (no chromaticity, no CSM clamp in this PS).
float3 orv = E - 2.0 * N * dot(E, N);
float3 ocube =
overlay.y > 0.0
? env_cube.SampleBias(smp, float3(-orv.x, -orv.y, orv.z),
misc.y).rgb
: float3(0.0, 0.0, 0.0);
float2 olm_uv = i.uv2 + 0.01 * nt.xy;
float3 olm = lightmap.SampleLevel(smp_clamp, olm_uv, 0.0).rgb;
float3 orefl = ocube * (olm * olm);
float NV = abs(dot(N, E));
float F = 0.25 + 0.75 * pow(1.0 - NV, 5.0);
// Ward anisotropic sun spec (spreads oc_p2.xy, roughness oc_p2.z).
float3 oud = normalize(float3(-N.z, 0.0, N.x));
float3 ovd = cross(N, oud);
float dNE = dot(N, E);
float dNL = dot(N, sh_sun.xyz);
float oax = oc_p2.z / oc_p2.x;
float oay = oc_p2.z / oc_p2.y;
float3 H = normalize(sh_sun.xyz + E);
float eu = dot(H, oud) / oax;
float ev = dot(H, ovd) / oay;
float oden = sqrt(abs(dNL * dNE)) * 12.566371 * oax * oay;
float ward = exp(-2.0 * (eu * eu + ev * ev) / (1.0 + dot(H, N))) /
max(oden, 1e-6);
float3 ocol = orefl * F + ward * oc_p0.rgb;
float3 oxe = (ocol * (fog_a * oc_p1.y) + fog_rgb) * expo;
return ToneOut(oxe, 1.0, false);
} }
// Baked ocean horizon sheet (fam 32, oceanreflection_defaultPS;
// verified per-pixel): squared sea texture x multiplier, fogged and
// tonemapped; alpha ramps with world height between the orf.z/orf.y
// bounds (the bright haze band fading in toward the horizon).
if (fam > 31.5 && fam < 32.5) { if (fam > 31.5 && fam < 32.5) {
float3 rcol = albedo.rgb * albedo.rgb * orf.x; return ShadeOceanReflection(i, dlin, fog_a, fog_rgb, expo);
float ra = saturate((i.rpos.y + cam_pos.y - orf.z) /
max(orf.y - orf.z, 1e-4));
float3 rxe = (rcol * fog_a + fog_rgb) * expo;
return ToneOut(rxe, ra, false);
} }
if (fam > 8.5 && fam < 12.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];
@ -1556,6 +678,9 @@ float4 ps_main(VSOut i) : SV_Target {
} }
return float4(PassGamma(lit), 1.0); return float4(PassGamma(lit), 1.0);
} }
float4 ps_main(VSOut i) : SV_Target {
return ShadePixel(i);
}
// ---- SSR reflection G-buffer (consumed by ssr.hlsl ps_march) -------------- // ---- SSR reflection G-buffer (consumed by ssr.hlsl ps_march) --------------
// Rendered after the main pass from the frame's reflective items only (env // Rendered after the main pass from the frame's reflective items only (env
// families 5/6/13 + water), half res, single sample, NO depth buffer: // families 5/6/13 + water), half res, single sample, NO depth buffer:

View file

@ -0,0 +1,136 @@
// Character families: the game's own lighting in LINEAR space (diffuse is
// gamma -> square it), then the exact tone chain from the disassembly and
// the postfx uber's 1.41 scene multiplier (which the empirical world
// shading already folds into its constants; without it characters sit
// ~30% darker than their surroundings, measured on an F11 A/B pair).
float4 ShadeCharacter(VSOut i, float4 albedo) {
float fam = cam_pos.w;
float3 dlin = albedo.rgb * albedo.rgb;
float3 cn = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
float ndl = saturate(dot(cn, ch_light.xyz));
float3 vd = -normalize(i.rpos);
float3 lin;
float out_a = 1.0;
if (fam > 5.5) {
// Traffic vehicles (vehicle.fx fam 6 body / vehicle_glass.fx fam 7
// windows, disassembled from vehicle_defaultPS): paint recolor where
// the diffuse green channel is below the mask threshold (red-channel
// mask * colorize_red + blue-channel mask * colorize_blue, the taxi
// yellow), key light + the livingworld flat ambient, phong specular
// along the reflected sun, and an environment-cube reflection scaled
// by fresnel and the gloss packed in the diffuse alpha. Glass keeps
// only the reflection terms (its tint rows are zero) and blends at
// the captured alpha. overlay.y > 0 = the material's cube resolved
// at t6 (same convention as water).
float3 sel;
if (fam < 6.5) {
sel = dlin.g > 0.001225
? dlin
: ch_tintA.rgb * dlin.r + ch_tintB.rgb * dlin.b;
} else {
sel = ch_tintA.rgb;
}
// DXN panel normal map (the material's `normal` channel, riding the
// macro slot; overlay.z > 0 = resolved). The vertex layout carries no
// tangent frame, so build a screen-space cotangent frame from the
// position/uv derivatives; it reproduces the authored panel shading
// including mirrored UV islands. Skipping the map entirely shades the
// hinged panels by their vertex normals, which face away from the sun
// - the dark ambient-blue "misdrawn shadow" that stopped at the door
// seam (verified against the ucode: flat map = the artifact, real
// map = the emulated car).
float3 vn = cn;
if (overlay.z > 0.5) {
float2 nm = macro.Sample(smp, i.uv).rg * 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, cn), dp1p = cross(cn, dp1);
float3 tt = dp2p * du1.x + dp1p * du2.x;
float3 bb = dp2p * du1.y + dp1p * du2.y;
float im = rsqrt(max(max(dot(tt, tt), dot(bb, bb)), 1e-12));
vn = normalize(nm.x * tt * im + nm.y * bb * im +
cn * sqrt(saturate(1.0 - dot(nm, nm))));
}
float vndl = saturate(dot(vn, ch_light.xyz));
float3 rfl = ch_light.xyz - 2.0 * dot(vn, ch_light.xyz) * vn;
// The sun spec is gated on N.L >= 0 (the ucode multiplies the spec
// term by an sge result); the cube reflection is not.
float spec = pow(saturate(dot(vd, -rfl)), max(ch_sh[0].w, 1.0)) *
(dot(vn, ch_light.xyz) >= 0.0 ? 1.0 : 0.0);
float fres = pow(1.0 - saturate(dot(vn, vd)), max(ch_light.w, 1.0));
float3 cube = float3(0.0, 0.0, 0.0);
if (overlay.y > 0.5) {
cube = env_cube.Sample(smp, reflect(-vd, vn)).rgb;
cube *= cube; // the PS consumes the cube squared (linear space)
}
// Gloss = the diffuse alpha SQUARED: the ucode squares the whole
// diffuse fetch (linear-space decode), alpha included; raw alpha
// over-specs ~5x and mottles the body panels.
float gloss = fam < 6.5 ? albedo.a * albedo.a : 1.0;
lin = sel * (ch_key.rgb * vndl + ch_amb.rgb) +
(spec * ch_sh[0].rgb + cube) * fres * gloss;
out_a = ch_misc.x;
} else if (fam > 3.5) {
// Hair (cac_hair / defaulthair): key on a wrapped N.L ramp + flat
// ambient, fresnel rim tint on a steeper ramp; strand coverage from
// the mesh's "alpha" channel at the raw second texcoord (bound at t4)
// - alpha-blended in the sorted sub-pass (hair drawn opaque is the
// blocky-helmet look).
float fres = pow(1.0 - saturate(dot(cn, vd)), max(ch_light.w, 1.0));
float3 hl = ch_key.rgb * (saturate(ndl * 0.75 + 0.25) + ch_amb.w) +
ch_tintB.rgb * fres * saturate(ndl * 1.75 + 0.25);
lin = dlin * hl;
out_a = saturate(decal_art.Sample(smp, i.uv2).r * ch_tintB.w);
} else if (fam > 2.5) {
// livingworld pedestrians: the diffuse is a stamp-mask atlas; red
// regions recolor with tintA, blue with tintB (judged in linear
// space; real-color regions have green above the threshold).
// The game's character PSes multiply the key light by the CSM shadow
// (tap >= ray = lit); characters are casters themselves, so an extra
// receiver bias suppresses self-shadow acne while the body-onto-board
// / body-onto-NPC shading survives (the sun-axis depth gap there is
// tens of cm). Without this the held skateboard, a big flat surface
// that is almost always inside the skater's own shadow, renders
// fully sunlit (near-white) against the emulated dark deck.
float csm = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, 0.012, cn, i.pos.xy),
SampleStaticSun(i.rpos + cam_pos.xyz, cn, i.pos.xy));
float3 sel = dlin.g > 0.001225
? dlin
: ch_tintA.rgb * dlin.r + ch_tintB.rgb * dlin.b;
lin = sel * (ch_key.rgb * ndl * csm + ch_amb.rgb);
// livingworld_stamp_defaultPS ends `max oC0.w, c21.x`: the entity's
// spawn/distance fade. Only visible when the item is routed to the
// blended sub-pass (alpha < 1); the opaque pass ignores it.
out_a = ch_misc.x;
} else {
// defaultcharacter / CAC pieces: key light + SH irradiance ambient,
// key gated by the CSM shadow (see the livingworld comment above).
float csm = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, 0.012, cn, i.pos.xy),
SampleStaticSun(i.rpos + cam_pos.xyz, cn, i.pos.xy));
if (ch_tintA.w > 0.0) {
dlin *= ch_tintA.rgb;
}
float3 irr = saturate(
ch_sh[0].rgb + cn.x * ch_sh[1].rgb + cn.y * ch_sh[2].rgb +
cn.z * ch_sh[3].rgb + (cn.x * cn.z) * ch_sh[4].rgb +
(cn.z * cn.y) * ch_sh[5].rgb + (cn.y * cn.x) * ch_sh[6].rgb +
(cn.z * cn.z) * ch_sh[7].rgb +
(cn.x * cn.x - cn.y * cn.y) * ch_sh[8].rgb);
lin = dlin * (ch_key.rgb * ndl * csm + irr * ch_amb.w);
// defaultcharacter/cacstamp PSes end `max oC0.w, c13.x / c22.x`:
// the entity's spawn fade (see the livingworld comment above).
out_a = ch_misc.x;
// character.alpha accessory (sunglass lens): coverage from the mesh's
// "alpha" channel at the raw second texcoord, like hair (cac_alphaPS:
// oC0.w = tf5(uv2).r * c22.x), routed to the blended sub-pass.
if (ch_misc.z > 0.5) {
out_a *= saturate(decal_art.Sample(smp, i.uv2).r);
}
}
// Exact tone chain: sqrt(0.5 * (max(x*E/4 + 0.75, 1) - sat(1 - x*E)^2)).
float E = max(ch_key.w, 0.01);
return ToneOut(lin * E, out_a, false);
}

View file

@ -0,0 +1,68 @@
// Shared shading helpers: screen-space / stored tangent frames and the
// output encode (the exact scene tone chain and its HDR pre-tonemap
// passthrough).
// 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);
}
}
// ---- Output encode --------------------------------------------------------
// Every exact branch ends in the same chain: x = the post-fog,
// post-exposure linear value ("xe"), then
// tonemap = max(x*0.25 + 0.75, 1) - sat(1 - x)^2
// out = sat(sqrt(0.5 * tonemap) * 1.41) (the uber's 1.41 fold)
// Under HDR=1 the branch returns xe itself into the float scene
// intermediate and the host post pass (hdr.hlsl ps_tonemap) applies the
// chain exactly once, which is what lets real bloom inject energy
// pre-tonemap. `reduced` = environmentdiffuse's cheap curve 1 - sat(1-x)^2;
// it equals the full curve evaluated at min(x, 1) (the two only differ past
// the x = 1 saturation point), so the HDR encode is a clamp.
float4 ToneOut(float3 xe, float a, bool reduced) {
#ifdef HDR
float3 x = max(xe, 0.0);
return float4(reduced ? min(x, 1.0) : x, a);
#else
float3 t1 = saturate(1.0 - xe);
float3 tm = reduced ? 1.0 - t1 * t1 : max(xe * 0.25 + 0.75, 1.0) - t1 * t1;
return float4(saturate(sqrt(max(tm * 0.5, 0.0)) * 1.41), a);
#endif
}
// Passthrough for values authored in the FINAL gamma space (the legacy
// empirical branches, water, debug visualizations): under HDR they encode
// as the tone chain's exact inverse, so the host tonemap restores them.
float3 PassGamma(float3 c) {
#ifdef HDR
float3 tm = c * c * (2.0 / (1.41 * 1.41));
float3 lo = 1.0 - sqrt(saturate(1.0 - tm));
float3 hi = 4.0 * tm - 3.0;
return lerp(lo, hi, step(1.0, tm)); // per-channel select (fxc + dxc)
#else
return c;
#endif
}

View file

@ -0,0 +1,137 @@
// dynamicobject.fx props (cam_pos.w = -(20 + variant): -21 default,
// -22 alphatest). Rigid movable objects (dispensers, dumpsters, benches,
// cans). Lit with the game's own dynamicobject model (verified exact
// against an offline ucode evaluation): key sun light + bounce +
// flat ambient, gated by min(CSM, max(static world shade, c8.w floor)),
// then fog -> exposure -> tonemap -> sqrt and the postfx uber 1.41.
// v2: misc.z
// carries the material bind flags (1 = base normal at t5, 2 = detail at
// 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).
float4 ShadeDynObject(VSOut i, float4 albedo) {
// dynobj variants only:
// -21/-22; the exact water family (-30) and sky (-40) pass through.
if (cam_pos.w < -21.5) {
clip(albedo.a - 0.1176); // dynamicobject.alphatest: ALPHAREF 30
}
float3 dlin = albedo.rgb * albedo.rgb;
float3 n = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
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 bounce = saturate(dot(wn, float3(-dyn_sun.x, dyn_sun.y, -dyn_sun.z)));
// CSM shadow (shared receiver rows at t7). extra_bias -1 = the game's
// own per-cascade dynamicobject receive bias (props are casters;
// without it their flat tops self-shadow and flicker).
float s = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, -1.0, wn, i.pos.xy),
SampleStaticSun(i.rpos + cam_pos.xyz, wn, i.pos.xy));
// Static world-shadow term: the game's baked building/tree shade,
// 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 lin = lighting * kd * 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.
float fdist = length(i.rpos);
float f1 = saturate(fdist * sh_fogp.x + sh_fogp.y);
if (sh_fogp.z != 1.0) {
f1 = pow(max(f1, 1e-6), sh_fogp.z);
}
float3 fog_rgb = sh_fogc.rgb * f1;
float fog_a = (1.0 + sh_fogc.a * f1) * dyn_misc.x; // x material multiplier
float3 xe = (lin * fog_a + fog_rgb) * dyn_sun.w;
return ToneOut(xe, 1.0, false);
}

View file

@ -0,0 +1,95 @@
// Per-frame shadow / lighting / material constant slice (b1). Shared by
// the scene pass (scene.hlsl) and the HDR post chain (hdr.hlsl) - the
// volumetric march samples sun visibility through the same receiver
// rows. The CPU staging in skate3_native_scene_gpu.cpp mirrors this
// layout; row offsets there and here must agree.
cbuffer S : register(b1) {
float4 sh_x; // light-space X row (xyz) + translation (w) [PS c0]
float4 sh_y; // light-space Y row [PS c3]
float4 sh_z; // depth row (height ramp) [PS c4]
float4 sh_c1; // cascade 1 scale.xy + offset.zw [PS c1]
float4 sh_c2; // cascade 2 scale.xy + offset.zw [PS c2]
float4 sh_color; // shadow color rgb [PS c8] + its luma in w
float4 sh_misc; // x = depth bias [PS c5.x], y = enable,
// zw = atlas dimensions (3*tile, tile)
// Exact world-shading frame rows (consumed by the env-family branch):
float4 sh_sun; // xyz = sun direction [PS c6], w = scene exposure [c10.x]
float4 sh_env; // x = material multiplier [PS c11.y], yz = tree lightmap
// scale/floor [tree PS c0.xy], w = tree tint mult [c4.y]
float4 sh_fogp; // xyz = global fog ramp scale/bias/exp [VS c5],
// w = proxyworld scale [proxy PS c3.y]
float4 sh_fogc; // fog color rgb + transmittance scale in w [VS c6]
// dynamicobject.fx frame-global lighting rows (see FrameScene::dynobj_rows).
float4 dyn_sun; // xyz = sun direction (PS c9), w = scene exposure (c13.x)
float4 dyn_amb; // xyz = flat ambient (c15.rgb), w = bounce scale (c15.w)
float4 dyn_misc; // x = material multiplier (c14.y),
// y = static world-shadow floor (c8.w)
// Character CSM receive biases (game char PS bank, finest cascade first)
// + enable in w: the exact 9-tap character shadow sampling
// (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;
// 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;
// flowingwateralpha m_params rows (captured per frame from the water PS
// bank; consumed by the exact water branch, cam_pos.w = -30):
float4 wat_p0; // m_params[0]: tangent-normal scale xzw, material mult y
float4 wat_p1; // m_params[1]: animated-UV scroll speeds (tap1 xy, tap2 zw)
float4 wat_p2; // m_params[2]: tap UV scales (tap1 xy, tap2 zw)
float4 wat_p3; // x = g_fAnimationTime, y = mask threshold,
// z = spec power, w = alpha floor
// ocean_defaultPS rows (exact ocean branch, cam_pos.w = -31): the PCA
// mean (pre-swizzled to R,G,B channel order) + the six weight rows
// (component-0/1 pairs for R, G, B) + m_params[0..2].
float4 oc_mean;
float4 oc_w0; // R, component 0
float4 oc_w1; // R, component 1
float4 oc_w2; // G, component 0
float4 oc_w3; // G, component 1
float4 oc_w4; // B, component 0
float4 oc_w5; // B, component 1
float4 oc_p0; // m_params[0]: ward spec color
float4 oc_p1; // m_params[1]: y = multiplier, z = tonedown scale,
// w = PCA-tap uv scale
float4 oc_p2; // m_params[2]: ward spread u/v (xy), roughness (z)
// oceanreflection_defaultPS row (fam 32): x = multiplier, y/z = the
// world-height alpha fade bounds.
float4 orf;
// Contact-hardening soft shadows (PCSS over the same cascade atlas; see
// SampleCsmShadowSoft): x = enable, y = tan of the sun's angular radius
// (penumbra width per meter of caster distance), z = max penumbra
// half-width in meters, w = static-caster receive bias in depth units
// (slope-scaled in the sampler; zero when static geometry is not in the
// atlas).
float4 sh_pcss;
// x = blocker-search radius in meters, y = meters per depth-map unit
// (1 / |sh_z.xyz|), z = light-space units per meter (|sh_x.xyz|, base
// cascade), w = minimum filter radius in physical atlas texels.
float4 sh_pcss2;
// Native static sun-shadow map transform (see SampleStaticSun): the
// world -> FAR-tile rows. uc = dot(nsm_x.xyz, wp) + nsm_x.w in [-1,1]
// (same for y), depth = dot(nsm_z.xyz, wp) + nsm_z.w in [0,1]. The near
// tile shares the origin, so its coordinate is exactly uc * nsm_p2.z.
float4 nsm_x;
float4 nsm_y;
float4 nsm_z;
// x = static shadow strength (0 = feature off), y = far-tile uc units
// per meter (1 / far radius), z = meters per depth unit, w = minimum
// filter radius in tile uc units.
float4 nsm_p;
// x = base receiver bias (depth units), y = slope-scaled bias term
// (depth units), z = reserved (cascade ratios are the shader-side
// kRatioInner/kRatioMid constants), w = tile size in pixels (tap
// clamping).
float4 nsm_p2;
};

View file

@ -0,0 +1,348 @@
// Shadow receivers: the game-exact CSM atlas sampler, its
// contact-hardening PCSS variant, and the native static sun-shadow map
// sampler. Expects the including file to declare the b1 rows
// (scene_frame_cb.hlsli) and the shadow_atlas / static_sun textures
// before this include.
// Native CSM atlas sample at a world position: finest covering cascade,
// s = saturate(infront + 1 - coverage). Returns 1 (lit) when uncovered or
// shadows are off. extra_bias suppresses receiver self-shadow acne on
// surfaces that are themselves casters (characters, held board), in DEPTH
// UNITS like the game's own bias constants (receiver c5.x, the dynobj
// literals); a world-metric bias was tried for the editor face band and
// REVERTED: the band was the HAT casting (the game's shadow passes skip
// it, see shadow_caster parity), not under-biasing, and the 0.144 m value
// erased the legit head-onto-neck shadow. A NEGATIVE extra_bias selects
// the game's dynamicobject receive bias instead: a per-cascade literal
// 0.007 (finest) / 0.015 (outer) replacing sh_misc.x
// (from the dynamicobject_defaultPS ucode). Props
// are casters themselves, and with only the world bias (c5.x = 0 in every
// capture) their flat tops compared against their own atlas depth; the
// lit/dark whole-surface flicker on benches, signs and sails as the
// camera-following cascades drifted frame to frame.
float SampleCsmShadow(float3 wp, float extra_bias) {
if (sh_misc.y <= 0.0) {
return 1.0;
}
float2 lsv = float2(dot(sh_x.xyz, wp) + sh_x.w, dot(sh_y.xyz, wp) + sh_y.w);
float2 luv = 0.0;
float casc = 0.0;
float2 l2 = lsv * sh_c2.xy + sh_c2.zw;
if (max(abs(l2.x), abs(l2.y)) < 0.99) { luv = l2; casc = 3.0; }
float2 l1 = lsv * sh_c1.xy + sh_c1.zw;
if (max(abs(l1.x), abs(l1.y)) < 0.99) { luv = l1; casc = 2.0; }
if (max(abs(lsv.x), abs(lsv.y)) < 0.99) { luv = lsv; casc = 1.0; }
if (casc <= 0.0) {
return 1.0;
}
float2 suv = float2(luv.x / 6.0 + (casc * 2.0 - 1.0) / 6.0,
luv.y * -0.5 + 0.5);
// Character receivers: the game's EXACT sampling (cacstamp_skin_nisPS
// ucode + live captured constants; offline-validated against the
// emulated frame). Reference = saturate(raw_depth - c9[cascade])
// (the per-cascade biases REPLACE the world receiver bias), NINE POINT
// taps at +-1 game-texel of the 512 tile (u step 1/1536 of the 3-tile
// atlas), binary sge each, averaged x 1/9 (literal c255.w). No coverage
// term: uncovered texels hold the far clear and compare lit.
if (extra_bias > 0.0 && sh_char.w > 0.0) {
float cbias = casc > 2.5 ? sh_char.z : (casc > 1.5 ? sh_char.y : sh_char.x);
float refd = saturate(dot(sh_z.xyz, wp) + sh_z.w - cbias);
// Tap offsets in game-map texels, BILINEAR-filtered like the game's
// fetches; the atlas blur dilates depth outward exactly so filtered
// compares stay valid across coverage edges; point-snapped taps
// quantized the penumbra into a stippled banding ("glisten"). Taps
// clamp half a texel inside the tile so filtering never bleeds the
// neighboring cascade.
float2 atlas_dim = float2(sh_misc.z, sh_misc.w);
float2 game_texel = float2(1.0 / 1536.0, 1.0 / 512.0);
float tile_x0 = (casc * 2.0 - 2.0) / 6.0;
float tile_x1 = casc * 2.0 / 6.0;
float acc = 0.0;
[unroll] for (int k = 0; k < 9; ++k) {
const float2 kOfs[9] = {float2(0.0, 0.0), float2(-1.0, -1.0),
float2(0.0, -1.0), float2(1.0, -1.0),
float2(-1.0, 0.0), float2(1.0, 0.0),
float2(-1.0, 1.0), float2(0.0, 1.0),
float2(1.0, 1.0)};
float2 s = suv + kOfs[k] * game_texel;
s.x = clamp(s.x, tile_x0 + 0.5 / atlas_dim.x, tile_x1 - 0.5 / atlas_dim.x);
s.y = clamp(s.y, 0.5 / atlas_dim.y, 1.0 - 0.5 / atlas_dim.y);
acc += shadow_atlas.Sample(smp_clamp, s).x >= refd ? 1.0 : 0.0;
}
return acc / 9.0;
}
float bias = extra_bias < 0.0 ? (casc > 1.5 ? 0.015 : 0.007)
: sh_misc.x + extra_bias;
float rd = dot(sh_z.xyz, wp) + sh_z.w - bias;
float2 sm2 = shadow_atlas.Sample(smp_clamp, suv);
return saturate((sm2.x >= rd ? 1.0 : 0.0) + (1.0 - sm2.y));
}
// Contact-hardening soft shadows (PCSS) over the same cascade atlas,
// enabled by sh_pcss.x. A blocker search estimates the average caster
// depth inside a world-space search radius; the penumbra half-width then
// follows the sun's angular size times the caster-to-receiver distance
// along the light axis (converted between world meters, depth-map units
// and light-space UV via the CPU-computed sh_pcss2 row scales), and a
// rotated poisson-disk PCF filters at that width; shadows are crisp at
// the contact point and soften with caster height, instead of the single
// fixed-width penumbra the blur chain bakes in. Tap semantics match the
// exact path (sat(infront + 1 - coverage); the blur pass dilates depth
// outward so filtered compares stay valid across coverage edges), and the
// per-class bias conventions of SampleCsmShadow carry over via extra_bias.
// The receiver normal drives an additional slope-scaled bias (sh_pcss.w)
// so large static casters, ground sheets, facades at grazing sun angles,
// never compare against their own atlas depth.
static const float2 kPcssTaps[12] = {
float2(-0.326212, -0.405805), float2(-0.840144, -0.073580),
float2(-0.695914, 0.457137), float2(-0.203345, 0.620716),
float2(0.962340, -0.194983), float2(0.473434, -0.480026),
float2(0.519456, 0.767022), float2(0.185461, -0.893124),
float2(0.507431, 0.064425), float2(0.896420, 0.412458),
float2(-0.321940, -0.932615), float2(-0.791559, -0.597705)};
float CsmSoftTap(float2 suv, float refd, float tile_x0, float tile_x1) {
// Taps clamp half a texel inside the tile so filtering never bleeds the
// neighboring cascade (same convention as the exact character taps).
suv.x = clamp(suv.x, tile_x0 + 0.5 / sh_misc.z, tile_x1 - 0.5 / sh_misc.z);
suv.y = clamp(suv.y, 0.5 / sh_misc.w, 1.0 - 0.5 / sh_misc.w);
float2 sm = shadow_atlas.Sample(smp_clamp, suv);
return saturate((sm.x >= refd ? 1.0 : 0.0) + (1.0 - sm.y));
}
// Poisson-rotation angle for the CSM atlas sampler: interleaved gradient
// noise of the SCREEN pixel: stable per pixel, smooth under motion. A
// world-position hash was rejected (sin(43758 * x) amplifies
// sub-millimeter float wobble of the reconstructed position into a fresh
// rotation every frame; penumbras boiled). The static sun-shadow sampler
// uses NO noise at all; its soft-compare Vogel kernel is smooth without
// it (verified offline), and every noise scheme read as speckle, stripes
// or grain there.
float PcssRotation(float2 px) {
return 6.2831853 *
frac(52.9829189 * frac(0.06711056 * px.x + 0.00583715 * px.y));
}
float SampleCsmShadowSoft(float3 wp, float extra_bias, float3 nrm,
float2 px) {
if (sh_pcss.x <= 0.5) {
return SampleCsmShadow(wp, extra_bias);
}
if (sh_misc.y <= 0.0) {
return 1.0;
}
float2 lsv = float2(dot(sh_x.xyz, wp) + sh_x.w, dot(sh_y.xyz, wp) + sh_y.w);
float2 luv = 0.0;
float casc = 0.0;
float2 cscale = float2(1.0, 1.0);
float2 l2 = lsv * sh_c2.xy + sh_c2.zw;
if (max(abs(l2.x), abs(l2.y)) < 0.99) { luv = l2; casc = 3.0; cscale = sh_c2.xy; }
float2 l1 = lsv * sh_c1.xy + sh_c1.zw;
if (max(abs(l1.x), abs(l1.y)) < 0.99) { luv = l1; casc = 2.0; cscale = sh_c1.xy; }
if (max(abs(lsv.x), abs(lsv.y)) < 0.99) { luv = lsv; casc = 1.0; cscale = float2(1.0, 1.0); }
if (casc <= 0.0) {
return 1.0;
}
// Receiver bias: the same per-class conventions as SampleCsmShadow
// (character per-cascade rows / dynobj literals / world bias), plus the
// slope-scaled static term.
float bias;
if (extra_bias > 0.0 && sh_char.w > 0.0) {
bias = casc > 2.5 ? sh_char.z : (casc > 1.5 ? sh_char.y : sh_char.x);
} else if (extra_bias < 0.0) {
bias = casc > 1.5 ? 0.015 : 0.007;
} else {
bias = sh_misc.x + extra_bias;
}
float ndl = 1.0;
if (dot(nrm, nrm) > 1e-4) {
ndl = abs(dot(normalize(nrm), sh_sun.xyz));
}
float slope = sqrt(saturate(1.0 - ndl * ndl)) / max(ndl, 0.12);
bias += sh_pcss.w * (1.0 + slope);
float refd = dot(sh_z.xyz, wp) + sh_z.w - bias;
float2 suv0 = float2(luv.x / 6.0 + (casc * 2.0 - 1.0) / 6.0,
luv.y * -0.5 + 0.5);
float tile_x0 = (casc * 2.0 - 2.0) / 6.0;
float tile_x1 = casc * 2.0 / 6.0;
// Light-space UV per meter for this cascade; luv -> atlas uv is
// (x / 6, y / 2) (three tiles across, [-1,1] vertical).
float2 uvpm = sh_pcss2.z * cscale;
float2 luv_to_suv = float2(1.0 / 6.0, 0.5);
float ang = PcssRotation(px);
float2 rot = float2(cos(ang), sin(ang));
// Blocker search over 7 taps in the search radius. The penumbra follows
// the NEAREST blocker (max depth below the reference), not the average:
// the search disk can straddle unrelated far casters; a receiver near a
// building's shadow region would average the building's depth into its
// own contact shadow and smear it to the max radius (the fully-blurred
// character shadow). The nearest blocker is the one whose penumbra the
// receiver is actually inside.
float2 rsearch = sh_pcss2.x * uvpm * luv_to_suv;
float bmax = -1.0;
{
float2 s = suv0;
s.x = clamp(s.x, tile_x0 + 0.5 / sh_misc.z, tile_x1 - 0.5 / sh_misc.z);
s.y = clamp(s.y, 0.5 / sh_misc.w, 1.0 - 0.5 / sh_misc.w);
float d = shadow_atlas.Sample(smp_clamp, s).x;
if (d < refd) { bmax = max(bmax, d); }
}
[unroll] for (int k = 0; k < 12; k += 2) {
float2 o = kPcssTaps[k];
o = float2(o.x * rot.x - o.y * rot.y, o.x * rot.y + o.y * rot.x);
float2 s = suv0 + o * rsearch;
s.x = clamp(s.x, tile_x0 + 0.5 / sh_misc.z, tile_x1 - 0.5 / sh_misc.z);
s.y = clamp(s.y, 0.5 / sh_misc.w, 1.0 - 0.5 / sh_misc.w);
float d = shadow_atlas.Sample(smp_clamp, s).x;
if (d < refd) { bmax = max(bmax, d); }
}
if (bmax < 0.0) {
// No caster inside the search footprint: every filter tap would
// compare lit (uncovered texels hold the far clear).
return 1.0;
}
float dist_m = max(refd - bmax, 0.0) * sh_pcss2.y;
float pen_m = min(dist_m * sh_pcss.y, sh_pcss.z);
// Filter radius: the penumbra in atlas units, floored at a fraction of a
// physical atlas texel so edges always stay filtered.
float2 rfilt = max(pen_m * uvpm, sh_pcss2.w * 2.0 / sh_misc.w) * luv_to_suv;
float acc = CsmSoftTap(suv0, refd, tile_x0, tile_x1);
[unroll] for (int j = 0; j < 12; ++j) {
float2 o = kPcssTaps[j];
o = float2(o.x * rot.x - o.y * rot.y, o.x * rot.y + o.y * rot.x);
acc += CsmSoftTap(suv0 + o * rfilt, refd, tile_x0, tile_x1);
}
return acc / 13.0;
}
// Native static sun-shadow receive (t10). The game's cascade transforms
// use a stylized near-vertical projection (fine for its own dynamic
// casters inside a ~12 m height window, ~40 degrees away from the
// material sun); tall static geometry needs a true sun-aligned
// projection, so static shade comes from this separate camera-centered
// ortho map instead of the CSM atlas. The term bottoms out at
// 1 - strength (nsm_p.x), keeping live static shade believable where the
// baked lighting's own sun disagrees, and fades out at the map edge so
// coverage never pops. Contact-hardening PCSS matching the atlas sampler
// (nearest-blocker search + rotated poisson PCF) when PCSS is enabled;
// a single compare otherwise. Uncovered texels hold the far clear = lit.
// Clamped tap into one tile of the three-tile static sun atlas (inner,
// mid, far left to right); taps never bleed across the tile seams.
float NsmTap(float2 suv, float refd, float tile_u0) {
float hx = 0.1666667 / nsm_p2.w; // half a texel in atlas u (3 tiles wide)
float hy = 0.5 / nsm_p2.w;
suv.x = clamp(suv.x, tile_u0 + hx, tile_u0 + 0.3333333 - hx);
suv.y = clamp(suv.y, hy, 1.0 - hy);
// Explicit LOD (the map is single-mip): the PCF loop below is dynamic
// and implicit-gradient sampling is not allowed in divergent flow.
return static_sun.SampleLevel(smp_clamp, suv, 0.0).x;
}
float SampleStaticSun(float3 wp, float3 nrm, float2 px) {
if (nsm_p.x <= 0.0) {
return 1.0;
}
float2 uc_far = float2(dot(nsm_x.xyz, wp) + nsm_x.w,
dot(nsm_y.xyz, wp) + nsm_y.w);
// Screen-pixel footprint in far-tile uc, BEFORE any divergent return
// (gradient op; the enclosing family branches are uniform per draw).
// Scales the filter floor with viewing distance so far shadows
// integrate over the map's texel grid instead of aliasing against it:
// the shadow-map analogue of texture mip filtering. Close-ups keep the
// 2-texel floor untouched.
float pxfoot = max(fwidth(uc_far.x), fwidth(uc_far.y));
float edge = max(abs(uc_far.x), abs(uc_far.y));
if (edge >= 0.99) {
return 1.0;
}
// Fade at the outer (far-tile) boundary only; the near->far handoff
// needs none (the far tile covers the near region too).
float fade = saturate((0.97 - edge) / 0.07);
// Cascade select (3 tiles sharing the origin, so each finer coordinate
// is an exact rescale of the far one). Ratios 6/2/1: the inner cascade
// trades peak density for reach; foliage dapple at 16x density read as
// over-detailed and only engaged very close; 6x at a third of the far
// radius is the calibrated compromise. Integer 1:2:6 chain keeps every
// tile on the far-texel snap grid. Keep in sync with
// RenderStaticSunMap's kRatioInner/kRatioMid.
const float kRatioInner = 6.0;
const float kRatioMid = 2.0;
float2 uc0 = uc_far * kRatioInner;
float2 uc1 = uc_far * kRatioMid;
float tile;
float2 uc;
float scale;
if (max(abs(uc0.x), abs(uc0.y)) < 0.99) {
tile = 0.0;
uc = uc0;
scale = kRatioInner;
} else if (max(abs(uc1.x), abs(uc1.y)) < 0.99) {
tile = 1.0;
uc = uc1;
scale = kRatioMid;
} else {
tile = 2.0;
uc = uc_far;
scale = 1.0;
}
float tile_u0 = tile / 3.0;
float ucpm = nsm_p.y * scale; // uc units per meter in this tile
float2 suv = float2(uc.x / 6.0 + (tile * 2.0 + 1.0) / 6.0,
uc.y * -0.5 + 0.5);
const float2 uc_to_suv = float2(1.0 / 6.0, 0.5);
float ndl = 1.0;
if (dot(nrm, nrm) > 1e-4) {
ndl = abs(dot(normalize(nrm), sh_sun.xyz));
}
float slope = sqrt(saturate(1.0 - ndl * ndl)) / max(ndl, 0.12);
float refd = dot(nsm_z.xyz, wp) + nsm_z.w - (nsm_p2.x + nsm_p2.y * slope);
if (sh_pcss.x <= 0.5) {
float s0 = NsmTap(suv, refd, tile_u0) >= refd ? 1.0 : 0.0;
return 1.0 - fade * nsm_p.x * (1.0 - s0);
}
// FIXED kernels throughout this sampler: no per-pixel noise. Verified
// offline against synthetic roof/wall/streetlamp depth maps at the real
// map scales: every noise scheme (world hash, interleaved gradient
// noise, white hash, radius jitter) renders the penumbra as speckle,
// stripes or grain, while fixed taps with the soft compares below give
// a clean smooth gradient (the penumbra fraction varies smoothly across
// receivers, and bilinear map sampling fills between taps). Denser
// blocker searches were also tried offline and REJECTED; unstable
// nearest-blocker picks around small casters draw dark veins.
float2 rsearch = sh_pcss2.x * ucpm * uc_to_suv;
float bmax = -1.0;
{
float d = NsmTap(suv, refd, tile_u0);
if (d < refd) { bmax = d; }
}
[unroll] for (int k = 0; k < 12; k += 3) {
float d = NsmTap(suv + kPcssTaps[k] * rsearch, refd, tile_u0);
if (d < refd) { bmax = max(bmax, d); }
}
if (bmax < 0.0) {
return 1.0;
}
float dist_m = max(refd - bmax, 0.0) * nsm_p.z;
float pen_m = min(dist_m * sh_pcss.y, sh_pcss.z);
// Filter floor: 2 physical texels, raised to ~2 screen-pixel footprints
// at distance (lab-calibrated distance anti-aliasing).
float floor_uc = max(nsm_p.w, 2.0 * pxfoot * scale);
float r_uc = max(pen_m * ucpm, floor_uc);
float2 rfilt = r_uc * uc_to_suv;
// Vogel-spiral PCF with SOFT depth compares: binary taps quantize the
// penumbra into N discrete levels and the stable per-pixel rotation
// arranges the quantization into a visible cross-hatch ("fine mesh")
// pattern; a continuous per-tap contribution over a small depth window
// turns the same taps into a smooth gradient. The window rides the
// receiver bias so contact shadows stay grounded. Wide penumbras (tall
// roof edges cap the radius) undersample the base kernel and re-weave
// the mesh: they spend a second Vogel ring instead; the branch is
// spatially coherent across a penumbra.
float w = max(nsm_p2.x * 2.0, 1e-5);
// Tap count follows the FINAL radius (penumbra or adaptive floor,
// whichever won); wide kernels undersample at 13 taps.
int n = r_uc > 0.3 * ucpm ? 26 : 13;
float acc = 0.0;
[loop] for (int j = 0; j < n; ++j) {
float r = sqrt((float(j) + 0.5) / float(n));
float a = float(j) * 2.399963;
float2 o = float2(cos(a), sin(a)) * r;
float d = NsmTap(suv + o * rfilt, refd, tile_u0);
acc += saturate((d - refd) / w + 0.5);
}
float s = acc / float(n);
return 1.0 - fade * nsm_p.x * (1.0 - s);
}

View file

@ -0,0 +1,150 @@
// Water families, hand-ported from the game's own pixel shaders and
// verified per-pixel against the ucode: flowingwateralpha (fam 30),
// the ocean surface (fam 31) and the baked ocean horizon sheet
// (fam 32). Callers pass the shared world-family locals (squared
// diffuse, fog terms, scene exposure).
// flowingwateralpha (fam 30): the canal/waterfall water shader,
// hand-ported from flowingwateralpha_defaultPS and verified per-pixel
// against the ucode (dual time-scrolled ripple-normal taps -> TBN ->
// lightmap-lit body + phong sun spec + cube reflection, then the
// standard fog/exposure/tonemap chain; alpha = the spec term's green
// channel, floored; reflection strength drives opacity over the bed).
float4 ShadeFlowingWater(VSOut i, float3 dlin, float fog_a,
float3 fog_rgb, float expo) {
float t = wat_p3.x;
float3 n1 = macro.Sample(smp, i.uv * wat_p2.xy + wat_p1.xy * t).rgb;
float3 n2 = macro.Sample(smp, i.uv * wat_p2.zw + wat_p1.zw * t).rgb;
float3 nsum = 2.0 * n1 + 2.0 * n2 - 2.0;
// vNormal = normalize(m_params[0].xzw * (n1 + n2)) in tangent space.
float3 vn = normalize(float3(nsum.x * wat_p0.x, nsum.y * wat_p0.z,
nsum.z * wat_p0.w));
float3 wn0 = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
// Stored frame: tanb = the mesh's usage-6 TANGENT here (see the water
// pack in DecodeMesh), sentinel sign = the unwrap sign.x;
// B = sign.x * cross(N, T), exactly the game's VS.
float3 wt, wb;
if (i.tanb.w > 0.1) {
wt = normalize(i.tanb.xyz);
wb = cross(wn0, wt) * (i.tanb.w > 0.6 ? 1.0 : -1.0);
} else {
ScreenTangentFrame(wn0, i.rpos, i.uv, wt, wb);
}
float3 wn = normalize(vn.x * wt + vn.y * wb + vn.z * wn0);
float3 vd = -normalize(i.rpos);
float s = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, 0.0, wn, i.pos.xy),
SampleStaticSun(i.rpos + cam_pos.xyz, wn, i.pos.xy));
// Lightmap at the unwrap + 0.01 * vNormal.xz (the ripple refraction
// wobble), console fetch semantics, min-clamped like every world
// family. White-fallback pages skip the clamp (see the env fetch).
float2 wlm_uv = i.uv2 + 0.01 * vn.xz;
float3 wlm = lightmap.SampleLevel(smp_clamp, wlm_uv, 0.0).rgb;
float3 lml = tint.r > 0.0 ? min(wlm * wlm, s + sh_color.rgb)
: wlm * wlm;
// GetTangentLight on the WORLD normal: fixed axis (0.58, 0.62, 0.39)
// with xy signs following the sun direction, x 1/normalize(axis).z.
float kd = (wn.x * 0.58 * sign(sh_sun.x) +
wn.y * 0.62 * sign(sh_sun.y) + wn.z * 0.39) * 2.39562;
float3 lin = lml * kd * dlin;
// Spec/reflection masks (t4): x = phong mask, z = reflection mask,
// thresholded by m_params[3].y. Zero until resolved: matte water.
float2 wmask = float2(0.0, 0.0);
if (overlay.x > 0.0) {
float4 wm4 = decal_art.Sample(smp, i.uv);
wmask = saturate(float2(wm4.x, wm4.z) - wat_p3.y);
}
// Phong vs the real sun, power m_params[3].z, tint (2.1, 1.8, 1.5),
// scaled by sat(clamped lightmap green - 0.1).
float3 wrl = 2.0 * wn * dot(wn, sh_sun.xyz) - sh_sun.xyz;
float ks = pow(max(saturate(dot(vd, wrl)), 1e-6), wat_p3.z);
float3 wspec =
ks * wmask.x * float3(2.1, 1.8, 1.5) * saturate(lml.g - 0.1);
// Cube reflection, same shape as fams 5/6: reflect(E, wN) with xy
// negated, luminosity lerped toward 1 by 0.3 * sat(4*mask - 2.6),
// x mask x 1.5. misc.y = the cube LOD bias (640p-gradient parity).
float3 wrv = vd - 2.0 * wn * dot(vd, wn);
float3 wcube =
overlay.y > 0.0
? env_cube.SampleBias(smp, float3(-wrv.x, -wrv.y, wrv.z),
misc.y).rgb
: float3(0.0, 0.0, 0.0);
float wrlum = 0.3 * saturate(4.0 * wmask.y - 2.6);
float3 wrefl = wcube * (lml.g + wrlum * (1.0 - lml.g)) * wmask.y * 1.5;
float3 wspec_term = wspec + wrefl;
lin += wspec_term;
float3 wxe = (lin * (fog_a * wat_p0.y) + fog_rgb) * expo;
return ToneOut(wxe, max(wspec_term.g, wat_p3.w), false);
}
// Exact ocean surface (fam 31, ocean_defaultPS; verified per-pixel
// against the ucode): PCA-compressed dual-component normal map, lerped
// toward flat by the overlay-scaled tonedown, cube reflection x squared
// lightmap x fresnel, ward anisotropic sun spec. OPAQUE (the game's PS
// outputs alpha 0 and draws without blending).
float4 ShadeOcean(VSOut i, float fog_a, float3 fog_rgb, float expo) {
float2 suv = i.uv * oc_p1.w;
float4 c0v = macro.Sample(smp, suv) * 2.0 - 1.0; // t3: comp 0
float4 c1v = detail_map.Sample(smp, suv) * 2.0 - 1.0; // t8: comp 1
// PCA normal in the shader's Z-UP basis; mean/weights are stored in
// 0..1 texture space and folded 2x-1 after the sum.
float3 pca = (float3(dot(c0v, oc_w0) + dot(c1v, oc_w1),
dot(c0v, oc_w2) + dot(c1v, oc_w3),
dot(c0v, oc_w4) + dot(c1v, oc_w5)) +
oc_mean.xyz) * 2.0 - 1.0;
// tonedown = 2 * m_params[1].z * sat(dot(c1v, wB1) + overlay.x);
// the ucode folds the second PCA blue dot into the overlay term.
float ovx = overlay.z > 0.0
? spec2_map.Sample(smp, i.uv * overlay.w).x
: 1.0;
float otd = (2.0 * oc_p1.z) * saturate(dot(c1v, oc_w5) + ovx);
float3 nt = normalize(lerp(float3(0.0, 0.0, 1.0), pca, otd));
nt = normalize(max(abs(nt), 0.001));
float3 N = float3(nt.x, nt.z, nt.y); // shader Z-up -> world Y-up
float3 E = -normalize(i.rpos);
// Cube reflection: reflect(E, N) with xy negated, x the SQUARED
// single-tap lightmap (no chromaticity, no CSM clamp in this PS).
float3 orv = E - 2.0 * N * dot(E, N);
float3 ocube =
overlay.y > 0.0
? env_cube.SampleBias(smp, float3(-orv.x, -orv.y, orv.z),
misc.y).rgb
: float3(0.0, 0.0, 0.0);
float2 olm_uv = i.uv2 + 0.01 * nt.xy;
float3 olm = lightmap.SampleLevel(smp_clamp, olm_uv, 0.0).rgb;
float3 orefl = ocube * (olm * olm);
float NV = abs(dot(N, E));
float F = 0.25 + 0.75 * pow(1.0 - NV, 5.0);
// Ward anisotropic sun spec (spreads oc_p2.xy, roughness oc_p2.z).
float3 oud = normalize(float3(-N.z, 0.0, N.x));
float3 ovd = cross(N, oud);
float dNE = dot(N, E);
float dNL = dot(N, sh_sun.xyz);
float oax = oc_p2.z / oc_p2.x;
float oay = oc_p2.z / oc_p2.y;
float3 H = normalize(sh_sun.xyz + E);
float eu = dot(H, oud) / oax;
float ev = dot(H, ovd) / oay;
float oden = sqrt(abs(dNL * dNE)) * 12.566371 * oax * oay;
float ward = exp(-2.0 * (eu * eu + ev * ev) / (1.0 + dot(H, N))) /
max(oden, 1e-6);
float3 ocol = orefl * F + ward * oc_p0.rgb;
float3 oxe = (ocol * (fog_a * oc_p1.y) + fog_rgb) * expo;
return ToneOut(oxe, 1.0, false);
}
// Baked ocean horizon sheet (fam 32, oceanreflection_defaultPS;
// verified per-pixel): squared sea texture x multiplier, fogged and
// tonemapped; alpha ramps with world height between the orf.z/orf.y
// bounds (the bright haze band fading in toward the horizon).
float4 ShadeOceanReflection(VSOut i, float3 dlin, float fog_a,
float3 fog_rgb, float expo) {
float3 rcol = dlin * orf.x;
float ra = saturate((i.rpos.y + cam_pos.y - orf.z) /
max(orf.y - orf.z, 1e-4));
float3 rxe = (rcol * fog_a + fog_rgb) * expo;
return ToneOut(rxe, ra, false);
}

File diff suppressed because it is too large Load diff