skate3recomp-multiplayer/src/native/shaders/scene.hlsl

976 lines
50 KiB
HLSL

// Face-normal shading uses the camera-relative world position: interpolating
// absolute world coordinates (hundreds of meters) destroys ddx/ddy precision
// and produces per-pixel noise.
cbuffer C : register(b0) {
row_major float4x4 world;
row_major float4x4 mvp;
float4 tint;
float4 cam_pos;
// Per-material color multiplier (w > 0 enables): CAS hair is a grayscale
// texture times the character's hair color.
float4 mat_tint;
// x = macroOverlayUVScale, y = macroOverlayOpacity, z > 0 = macro overlay
// texture bound at t3, w > 0 = environment.decal item with its `decal`
// art bound at t4 (composited in-shader over the diffuse, exactly like
// the game's decalenvironment PS: lerp by the art's alpha; opaque).
// Macro and decal art are INDEPENDENT: decal ground/wall sections carry
// the same macrooverlay as their non-decal neighbors, sharing one slot
// dropped the macro grime there and rendered alternating plaza sections
// ~1.4x too bright (the large-scale ground checkerboard).
float4 overlay;
// x > 0 = environment.transparent item (alpha-blended sub-pass): shading
// follows the game's transparentenvironment.fx; the opaque pass's
// alpha-test turned the soft mist gradients into solid white cloud blobs.
// yzw = the global distance-fog RAMP (scale/bias/exponent from main-pass
// VS c5), and mat_tint doubles as the linear-space fog COLOR (rgb +
// transmittance scale in w, VS c6) for transparent items; the root
// signature is capped at 64 DWORDs, so fog rides in slots transparent
// items never use otherwise. Fog is currently applied to transparent
// items only (the km-distant mist sheets; everything else we render is
// near enough for fog to be negligible).
// Fam 14 (scrollincandescent, opaque, so every fog slot is free):
// xy = the period-wrapped UV scroll offset, z = the material multiplier.
float4 misc;
};
// Per-frame dynamic-shadow (CSM) receiver constants, captured from the
// game's world-material PIXEL banks.
#include "scene_frame_cb.hlsli"
// Character-family lighting (defaultcharacter.fx and friends): canonical
// per-draw rows captured from the guest PIXEL constant bank at palette
// capture (CaptureCharLighting has the per-family register maps; the math
// below was validated offline by executing the game's own pixel shaders).
// Enabled per draw via cam_pos.w = family (0 = not a character / capture
// failed -> the legacy empirical shading below).
cbuffer CH : register(b2) {
float4 ch_light; // xyz = sun direction, w = hair fresnel power
float4 ch_key; // rgb = key (sun) color, w = exposure
float4 ch_amb; // rgb = flat ambient, w = SH ambient multiplier / hair ambient
float4 ch_sh[9]; // SH irradiance rows, pre-scaled (see capture);
// vehicles keep spec color + power in row 0 instead
float4 ch_tintA; // CAC diffuse tint / livingworld red-mask tint (w = apply)
float4 ch_tintB; // livingworld blue-mask tint / hair fresnel tint (w = strand-alpha scale)
float4 ch_misc; // x = alpha out, y = family, z = lens-alpha flag,
// w = rim fresnel power
float4 ch_ks; // rgb = key spec color, w = power (0 = spec rows absent)
float4 ch_rs; // rgb = rim spec color, w = power
float4 ch_rim; // rgb = rim light color, w = key-spec fresnel power
};
Texture2D<float4> diffuse : register(t0);
Texture2D<float4> lightmap : register(t1);
Texture2D<float4> macro : register(t3);
Texture2D<float4> decal_art : register(t4);
// Paired second descriptor of the t4 table: the reflective families'
// (fam 5/6) normal map. Only valid, and only sampled, when overlay.w == 4.
Texture2D<float4> normal_map : register(t5);
Texture2D<float2> shadow_atlas : register(t7);
TextureCube<float4> env_cube : register(t6);
// World-shading v2 material maps (the t8/t9 pair table): the detail normal
// map (t8, sampled at uv * misc.w) and the spec/ecc masks (t9). Sampled
// only when the misc.z bind flags say so (env fams 1-4 and the
// dynamicobject families; see those branches).
Texture2D<float4> detail_map : register(t8);
Texture2D<float4> spec2_map : register(t9);
// Native static sun-shadow map (third entry of the t8/t9 table): a single
// camera-centered ortho depth map of the STATIC world along the material
// sun, rendered natively each frame. Sampled by SampleStaticSun as an
// additional receive term on every lit branch.
Texture2D<float2> static_sun : register(t10);
// Raw bone palette: 3 float4 rows per bone, column-vector affine [R | t],
// applied with explicit dots (StructuredBuffer<float4x4> default packing is
// column-major and would silently transpose the matrices).
StructuredBuffer<float4> bones : register(t2);
SamplerState smp : register(s0);
// s1 = bilinear CLAMP (shared with the 2D pass). Decal art must clamp: the
// art UV runs far outside [0,1] across big ground sheets and the art's
// transparent border keeps everything outside the single placement clear;
// wrap sampling tiled the graffiti across the whole plaza.
SamplerState smp_clamp : register(s1);
struct VSOut {
float4 pos : SV_Position;
float3 rpos : TEXCOORD0;
float2 uv : TEXCOORD1;
float2 uv2 : TEXCOORD2;
float3 nrm : TEXCOORD3;
float2 uv3 : TEXCOORD4;
// Stored tangent frame (world-shading v2): xyz = the mesh's authored
// binormal (world-rotated; zero when the mesh carries none), w = the
// tangent handedness sign. Rides the static meshes' otherwise-unused
// blend-weight bytes (see DecodeMesh).
float4 tanb : TEXCOORD5;
};
VSOut vs_main(float3 p : POSITION, float2 uv : TEXCOORD0, float2 uv2 : TEXCOORD1,
float4 bw : BLENDWEIGHT0, uint4 bi : BLENDINDICES0,
float3 nrm : NORMAL0, float2 uv3 : TEXCOORD2) {
VSOut o;
float4 mp = float4(p, 1.0);
float3 n = nrm;
// tint.g > 0 marks a skinned item: the bone palette (row-vector matrices)
// maps model space to world space; mvp is then just view*proj.
float wsum = dot(bw, float4(1, 1, 1, 1));
if (tint.g > 0.0 && wsum > 0.001) {
float3 skinned = float3(0, 0, 0);
float3 sn = float3(0, 0, 0);
// Guest blend indices are plain bone numbers (verified live: byte
// streams like 02 00 03 01); bone k = palette rows 3k..3k+2.
[unroll] for (int k = 0; k < 4; ++k) {
uint row = bi[k] * 3u;
skinned += bw[k] * float3(dot(mp, bones[row]), dot(mp, bones[row + 1]),
dot(mp, bones[row + 2]));
sn += bw[k] * float3(dot(n, bones[row].xyz), dot(n, bones[row + 1].xyz),
dot(n, bones[row + 2].xyz));
}
mp = float4(skinned / wsum, 1.0);
n = sn;
} else {
n = mul(n, (float3x3)world);
}
o.pos = mul(mp, mvp);
o.rpos = mul(mp, world).xyz - cam_pos.xyz;
o.uv = uv;
o.uv2 = uv2;
o.nrm = n;
o.uv3 = uv3;
// Stored binormal + handedness from the blend-weight bytes (statics
// only; skinned items use those bytes for actual weights). w passes
// RAW: ~0 = no stored frame, ~0.39 = negative handedness, ~0.78 =
// positive (the DecodeMesh sentinel bytes 0/100/200).
float3 sb = bw.xyz * 2.0 - 1.0;
o.tanb = tint.g > 0.0 ? float4(0.0, 0.0, 0.0, 0.0)
: float4(mul(sb, (float3x3)world), bw.w);
return o;
}
// ---- Graphics build-up showcase -------------------------------------------
// sh_v2.yzw carry the showcase split state: y/z = the LAYER MASK shown
// left/right of the vertical split at w (in output pixels), encoded as
// 256 + mask so an all-zero row (showcase off) is unambiguous. Mask bits
// (the contract shared with hdr.hlsl, ssr.hlsl and the CPU sequencer,
// see kShowcaseLayers in skate3_native_scene.h):
// 1 albedo textures 2 baked lighting 4 materials & surface detail
// 8 dynamic shadows 16 ambient occlusion 32 reflections (SSR)
// 64 volumetrics 128 bloom
// 256 decals & grime 512 dynamic entities
// 1024 blackout: the run's opening/closing bookends (ps_main renders every
// draw black; doubles as the recording cut marker), not an orderable layer.
// The material bits are progressive looks (materials subsumes lighting
// subsumes albedo); the rest are independent, so the sequencer can reveal
// them in any order or grouping. 256/512 are SUBTRACTIVE layers: their
// content belongs to the normal frame and stays hidden until the bit
// reveals (the sequencer folds their bits into every step when the layer
// is not part of the run). Returns -1 when the showcase is off.
// The showcase code compiles in only for the SHOWCASE=1 shader variants,
// which the pipeline family swaps in for the duration of a run. The default
// build folds the mask to the off value, so every showcase branch
// dead-strips and the program matches the pre-showcase compile exactly.
#ifndef SHOWCASE
#define SHOWCASE 0
#endif
int ShowcaseMask(float px_x) {
#if SHOWCASE
float v = px_x < sh_v2.w ? sh_v2.y : sh_v2.z;
return v < 255.5 ? -1 : (int)(v + 0.5) - 256;
#else
return -1;
#endif
}
// True when the given layer bit is revealed on this pixel's side of the
// split (or the showcase is off entirely).
bool ShowcaseLayerOn(float px_x, int bit) {
int m = ShowcaseMask(px_x);
return m < 0 || (m & bit) != 0;
}
#include "scene_shadows.hlsli"
#include "scene_common.hlsli"
#include "scene_char.hlsli"
#include "scene_dynobj.hlsli"
#include "scene_water.hlsli"
// The full material shading for every family (see ps_main below; the
// showcase wrapper substitutes the early build-up stages' looks over this
// result's rgb while keeping its alpha).
float4 ShadePixel(VSOut i) {
if (tint.a > 0.0) {
return float4(PassGamma(tint.rgb), tint.a);
}
float4 albedo = diffuse.Sample(smp, i.uv);
// Alpha-tested foliage/fences; opaque formats sample alpha = 1. Character
// diffuse packs GLOSS in alpha; never clip characters. tint.g > 0 marks
// them: set for bones-bound skinned items AND for ropa cloth garments
// rendered rigid (sim-active player tees, clipping their gloss alpha
// discarded every pixel: the invisible-shirt bug; their decode writes
// zero blend weights, so the VS skinning branch stays off).
if (tint.g == 0.0 && overlay.w < 0.5 && cam_pos.w > -20.5) {
// environment.transparent alpha-tests its SQUARED alpha at ref 16/255
// (transparentenvironment.xml: ALPHAREF 16, PS outputs a = diffuse.a^2).
// Exact env families (cam_pos.w < 0) use the game's world ALPHAREF 30.
// dynamicobject items (cam_pos.w <= -21) are excluded here and clip in
// their own branch (only the .alphatest variant tests, at ALPHAREF 30).
// Fam 13 (reflective_trans glass, cam_pos.w = -13) never alpha-tests;
// it alpha-BLENDS in the sorted sub-pass. Fam 14 (scrollincandescent)
// never alpha-tests either (its technique sets no test state, and its
// PS ignores the diffuse alpha entirely).
if (cam_pos.w > -12.5 || cam_pos.w < -14.5) {
float aref = cam_pos.w < -0.5 ? 0.1176 : 0.35;
clip(misc.x > 0.0 ? albedo.a * albedo.a - 0.0627 : albedo.a - aref);
}
}
// Exact sky dome (cam_pos.w = -40; sky_defaultPS transcribed from the
// Skate 3 ucode in a live capture). The emulated frame's big sun
// glow is computed IN the dome shader: a 1D radial gradient (the sky
// material's `specular` channel, bound at t4 here) indexed by the sine of
// the angle between the dome direction and the sun, its rgb^2 amplified
// by 1/sat(a + 0.01), the alpha falloff makes the core HDR-bright, then
// added to the squared panorama and run through the standard exposure/
// tonemap/sqrt chain (no fog on the sky) and the postfx uber 1.41.
// mat_tint.xyz = sun dir, mat_tint.w = the level sky elevation;
// overlay.x = sun angular scale, overlay.y = pre-tone multiplier,
// misc.y = scene exposure.
if (cam_pos.w < -39.5 && cam_pos.w > -40.5) {
// The dome mesh is camera-relative: world = mesh + (cam.x, sky_h,
// cam.z), so the shader's dome-local direction (sky.fx In.vPos) is
// rpos + (0, cam.y - sky_h, 0).
float3 dome = normalize(i.rpos + float3(0.0, cam_pos.y - mat_tint.w, 0.0));
float3 lin = albedo.rgb * albedo.rgb;
float dotPL = saturate(dot(dome, mat_tint.xyz));
float sinA = sqrt(saturate(1.0 - dotPL * dotPL));
float4 sun = decal_art.Sample(smp_clamp, float2(sinA / overlay.x, 0.5 / 16.0));
lin += sun.rgb * sun.rgb / saturate(sun.a + 0.01);
float3 xe = lin * overlay.y * misc.y;
return ToneOut(xe, 1.0, false);
}
// dynamicobject.fx props: scene_dynobj.hlsli. Variants -21/-22 only;
// the exact water families (-30..) and the sky (-40) pass through.
if (cam_pos.w < -20.5 && cam_pos.w > -25.5) {
return ShadeDynObject(i, albedo);
}
// Character families (defaultcharacter / CAC / livingworld / hair /
// vehicles): scene_char.hlsli.
if (cam_pos.w > 0.5) {
return ShadeCharacter(i, albedo);
}
// Exact world-material families (cam_pos.w = -family): hand-ported from
// the game's own pixel shaders and verified per-pixel against them with
// an offline ucode interpreter. All texture
// colors linearize IN-SHADER as x^2 (the fetch signs are unsigned on every
// world texture). Families: 1 baseenvironment, 2 defaultenvironment,
// 3/4 decalenvironment(_tileable), 5/6 reflective(_simple), 7 alphatest,
// 8 environmentdiffuse, 9/10 tree(animate), 11 proxyworld,
// 12 incandescent. v1 runs with NEUTRAL normal/detail maps (kd is the
// exact flat-map constant 0.39 * 2.39562); spec/reflection masks bind at
// t4 (overlay.w == 3) on families without decal art.
if (cam_pos.w < -0.5) {
float fam = -cam_pos.w;
float3 dlin = albedo.rgb * albedo.rgb;
// Global distance fog (VS c5/c6, captured per frame): every world PS
// ends with col * fog.a + fog.rgb before exposure/tonemap.
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;
float expo = sh_sun.w;
float3 lin;
float out_a = 1.0;
bool reduced_tone = false;
// Water families (30 flowingwateralpha / 31 ocean /
// 32 oceanreflection): exact ports in scene_water.hlsli.
if (fam > 29.5 && fam < 30.5) {
return ShadeFlowingWater(i, dlin, fog_a, fog_rgb, expo);
}
if (fam > 30.5 && fam < 31.5) {
return ShadeOcean(i, fog_a, fog_rgb, expo);
}
if (fam > 31.5 && fam < 32.5) {
return ShadeOceanReflection(i, dlin, fog_a, fog_rgb, expo);
}
if (fam > 8.5 && fam < 12.5) {
// tree/treeanimate: D^2 * max(lm^2, floor) * scale [* tint mult];
// proxyworld/incandescent: D^2 * scale. No shadow receive, no kd, no
// material multiplier on the fog term.
if (fam < 10.5) {
// Console lightmap semantics: bilinear, clamped, mip 0 (see the
// main env fetch below).
float3 lmg = lightmap.SampleLevel(smp_clamp, i.uv2, 0.0).rgb;
lin = dlin * max(lmg * lmg, sh_env.z) * sh_env.y;
if (fam < 9.5) {
lin *= sh_env.w;
}
out_a = albedo.a;
} else {
lin = dlin * (fam < 11.5 ? sh_fogp.w : 1.0);
}
} else if (fam > 13.5 && fam < 14.5) {
// scrollincandescent (the stadium LED chyron band): emissive scrolled
// diffuse. The game's VS adds g_fAnimationTime * (uSpeed, vSpeed) to
// the texcoord (misc.xy carries that offset, period-wrapped CPU-side);
// its PS is one diffuse fetch -> D^2 * m_params[0].y (misc.z) ->
// the shared fog/exposure/tone tail. No lightmap term, no kd, no
// shadow receive, no material multiplier on the fog term
// (ucode-exact: scrollincandescent_defaultPS).
float3 sd = diffuse.Sample(smp, i.uv + misc.xy).rgb;
lin = sd * sd * misc.z;
} else {
// Environment families: macro overlay (0.5-neutral, fades under decal
// art), linear decal composite, lightmap squared and min-clamped
// against (CSM s + shadow color), kd, phong spec vs the shader's
// fixed literal light, cube reflection on 5/6.
float3 ov = float3(1.0, 1.0, 1.0);
// Decals-&-grime build-up layer (bit 256): macro weathering and decal
// art both render neutral until revealed.
bool sc_decals = ShowcaseLayerOn(i.pos.x, 256);
// Fam 13 (transparentenvironmentreflective) carries no macro term.
if (overlay.z > 0.0 && fam < 12.5 && sc_decals) {
float3 mo = macro.Sample(smp, i.uv * overlay.x).rgb;
ov = saturate((mo - 0.5) * overlay.y + 0.5);
}
if (fam > 2.5 && fam < 4.5 && overlay.w > 0.5 && sc_decals) {
// overlay.w == 0 = art unresolved (white fallback alpha 1 would
// whitewash the whole surface).
float4 dk = overlay.w > 1.5 ? decal_art.Sample(smp, i.uv3)
: decal_art.Sample(smp_clamp, i.uv3);
dlin = lerp(dlin, dk.rgb * dk.rgb, dk.a);
// The macro weathering overlay applies OVER the composited art:
// ApplyOverlay(cOverlay, ApplyDecal(...)) in the decalenvironment
// source. The previous `ov = lerp(1, ov, 1-dk.a)` fade rendered the
// paint unweathered: on the PCU Library ramp stencils the measured
// native/emulated brightness error was 1.19x at paint alpha~1,
// 1.05x at ~0.35 and 1.0x off-patch: exactly 1/ov for ov~0.84,
// the no-fade model (measured from a matched A/B capture pair).
}
float3 dcol = dlin * ov;
// CSM shadow term s = sat(infront + 1 - coverage) from the native
// atlas (SampleCsmShadow bias/cascade conventions; the soft variant
// adds the contact-hardening filter when PCSS is enabled), min'd
// with the native static sun-shadow term.
float s = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, 0.0, i.nrm, i.pos.xy),
SampleStaticSun(i.rpos + cam_pos.xyz, i.nrm, i.pos.xy));
// Lightmap fetch = the console's semantics: BILINEAR, CLAMPED, mip 0.
// The composed atlas pages are single-level on console and the fetch
// constants carry clamp_x/clamp_y = 2. Sampling them with the aniso-8
// WRAP sampler over our generated mip chain averaged NEIGHBORING
// atlas cells together at grazing angles (deep aniso LOD), and at the
// page border the wrap pulled in the opposite edge of the page: the
// reflective_trans canopy slope (cells at v ~ 0.996, white-cliff
// cells wrapping in from v ~ 0) glowed ~2x bright while the right
// awning went dark, with the decode, UVs and constants all verified
// exact (diagnosed via the mode-7 raw-lightmap isolation view).
float3 lmg = lightmap.SampleLevel(smp_clamp, i.uv2, 0.0).rgb;
// F12 isolation mode 7: visualize the RAW lightmap sample; mode 8:
// visualize the lightmap unwrap coordinate (frac(uv2*16) in rg).
// Debug taps live on fams 5/6/13 only; on fams 1-4 misc.z carries
// the v2 material bind flags instead (family-gated below).
if (fam > 4.5 && abs(misc.z - 7.0) < 0.5) {
return float4(PassGamma(lmg), 1.0);
}
if (fam > 4.5 && abs(misc.z - 8.0) < 0.5) {
return float4(PassGamma(float3(frac(i.uv2 * 16.0), 0.0)), 1.0);
}
// Mode 9: lightmap-resolve status; RED = a real lightmap is bound
// (tint.r set by the C++ resolve), BLUE = white fallback in t1.
if (fam > 4.5 && abs(misc.z - 9.0) < 0.5) {
return float4(PassGamma(float3(tint.r, 0.0, 1.0 - tint.r)), 1.0);
}
// tint.r == 0 = the real lightmap has not resolved yet (first-sight
// decode in flight; t1 = the white fallback). Min-clamping the
// fallback against the CSM term rendered in-shadow surfaces as BLACK
// patches for the decode window (the ramp-stencil "black square
// flash"); serve unshadowed brightness until the real page lands.
float3 lml = tint.r > 0.0 ? min(lmg * lmg, s + sh_color.rgb) : lmg * lmg;
// GetTangentLight (world-shading v2). vnd is the tangent-space mapped
// normal from the material's base normal map (t5) + detail map (t8 at
// uv * misc.w): xy = 2*base + 2*detail - 2, z = 2*base.z - 1.
// baseenvironment/decal normalize vnd for the kd dot; default-
// environment (plain normal map, no detail pair) uses it raw. Fams
// 1-4 carry bind flags in misc.z (1 = base normal at t5, 2 = detail
// at t8, 4 = spec2ch at t9); fams 5/6 derive vnd from the reflective
// path's nt below. Without a normal map the exact flat-map fold
// 0.39 * 2.39562 = 0.93429 applies (the v1 constant).
uint v2f = fam < 4.5 ? (uint)(misc.z + 0.5) : 0u;
float kd = 0.93429;
float3 wn = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
float3 vnd_raw = float3(0.0, 0.0, 1.0); // wn mapping (max-z clamped)
float3 vnd = float3(0.0, 0.0, 1.0); // kd dot (family-normalized)
bool have_vnd = false;
if ((v2f & 1u) != 0u) {
float3 nmv = normal_map.Sample(smp, i.uv).rgb;
float2 dxy = (v2f & 2u) != 0u
? detail_map.Sample(smp, i.uv * misc.w).rg
: float2(0.5, 0.5);
vnd_raw = float3(nmv.xy * 2.0 + dxy * 2.0 - 2.0, nmv.z * 2.0 - 1.0);
vnd = (fam < 1.5 || fam > 2.5) ? normalize(vnd_raw) : vnd_raw;
have_vnd = true;
}
// Tangent frames shared by kd and the reflective normal mapping.
// Two variants of the same construction:
// - tt_s/bb_s = the RAW screen-space UV-gradient frame
// (ScreenTangentFrame: exact for the kd sign terms under any UV
// orientation).
// - tt/bb = analytic world-up axes carrying the screen frame's
// signs; degree-accurate where the mapping IS world-up aligned
// (building facades, calibrated for the glass reflections).
// Transferring signs onto misaligned axes is METASTABLE when the
// UV grid sits ~90 deg off world-up: the transfer dots cross zero
// along a curved wall and kd banded light/dark around the bowl
// transition (seen in a matched F11 capture pair), which is why
// kd and the fam 1-4 normal mapping use the screen frame instead.
float3 tt = float3(1.0, 0.0, 0.0), bb = float3(0.0, 1.0, 0.0);
float3 tt_s = tt, bb_s = bb;
if (have_vnd || (overlay.w > 3.5 && abs(misc.z - 3.0) > 0.5)) {
ScreenTangentFrame(wn, i.rpos, i.uv, tt, bb);
tt_s = tt;
bb_s = bb;
float3 bb2 = float3(0.0, 1.0, 0.0) - wn * wn.y;
float lb2 = length(bb2);
if (lb2 > 0.05) {
bb2 /= lb2;
float3 tt2 = cross(bb2, wn);
tt = tt2 * (dot(tt2, tt) >= 0.0 ? 1.0 : -1.0);
bb = bb2 * (dot(bb2, bb) >= 0.0 ? 1.0 : -1.0);
}
}
// kd axes: stored per-vertex frame with the screen fallback (KdAxes).
float3 kt, kb;
KdAxes(i.tanb, wn, tt_s, bb_s, kt, kb);
if (have_vnd) {
// The mapped world normal feeds the spec mirror below; matte spec
// is broad-lobed, so exact axis precision matters less than the
// per-island signs (the analytic substitution exists for
// mirror-sharp glass).
wn = normalize(vnd_raw.x * kt + vnd_raw.y * kb +
wn * max(vnd_raw.z, 0.05));
kd = (vnd.x * 0.58 * sign(dot(kt, sh_sun.xyz)) +
vnd.y * 0.62 * sign(dot(kb, sh_sun.xyz)) +
vnd.z * 0.39) * 2.39562;
}
// misc.z = 3 (F12 reflection isolation): force the flat normal.
// Only the reflective families (5/6/13) ever carry overlay.w == 4;
// fams 1-4 signal their normal map through the misc.z flags above.
if (overlay.w > 3.5 && abs(misc.z - 3.0) > 0.5) {
// Per-pixel normal map (t5, paired descriptor): the real PS
// (baseenvironmentreflective_defaultPS) reflects off the
// normal-mapped normal: tangent normal = 2*(n + detail - 1) on
// xy, 2*n.z - 1 on z (the material's detail map is a constant
// neutral 16x16, folded here as 0.5). With the FLAT vertex normal
// every panel of a glass facade reflects the same tiny cube
// region: one giant magnified smear of the plaza cube's
// lamp-heads/trees (the "streetlight head" artifact, ucode-traced
// on a captured pixel; flat N lands on the
// face-0 tree/lamp texels, the mapped N tilts onto sky). The
// per-panel tilts break the reflection up exactly like the
// emulated frame. Screen-space cotangent frame (same shape as
// the vehicle DXN branch; the world VS carries no tangent
// attributes we decode).
// F12 mode 6: the slider drives the normal-map LOD bias live;
// the console fetches the nm at its 640p gradient LOD (blurrier,
// weaker bump tilts), so the matching bias is the remaining
// reflection-rotation candidate. Default stays SHARP (bias 0):
// an unconditional misc.y bias visibly degraded the glass
// reflections; tune with the
// slider first, promote the found value to a default after.
float nm_bias = misc.z > 5.5 ? misc.w : 0.0;
float3 nmv = normal_map.SampleBias(smp, i.uv, nm_bias).rgb;
// Exact composition: xy = 2*normal + 2*detail - 2, z = 2*n.z - 1.
// The detail map is a CONSTANT 16x16 (0.514, 0.506), NOT the
// formula's 0.5 neutral, so its fold is a constant tangent tilt.
// The tilt rides the two F12 trim sliders (packed in misc.x;
// defaults = the exact fold) so the residual reflection rotation
// can be dialed live against the emulated frame.
float trim_yi = floor(misc.x / 1000.0);
float2 trim = float2(misc.x - trim_yi * 1000.0 - 500.0, trim_yi - 500.0) *
0.001;
float3 nt = float3(nmv.xy * 2.0 - 1.0 + trim, nmv.z * 2.0 - 1.0);
// (Frame construction hoisted above; the calibration notes ride
// with it; tt/bb here are the shared axes.)
wn = normalize(nt.x * tt + nt.y * bb + wn * max(nt.z, 0.05));
// v2: the reflective families share the kd term; fam 5
// (reflective) is a base-family material and normalizes vnd for
// the dot like baseenvironment; fam 6 (reflective_simple) uses
// it raw like the other _simple family. kt/kb = the shared kd
// axes computed above (stored frame when the mesh carries one),
// built from the pre-mapping geometric normal.
float3 v56 = fam < 5.5 ? normalize(nt) : nt;
kd = (v56.x * 0.58 * sign(dot(kt, sh_sun.xyz)) +
v56.y * 0.62 * sign(dot(kb, sh_sun.xyz)) +
v56.z * 0.39) * 2.39562;
}
// Fam 13 has no kd term at all; its body is D^2 * lml * ALPHA,
// premultiplied once in the shader on top of the a^2 blend factor
// (verified 0.0-error vs the ucode).
lin = fam > 12.5 ? lml * dcol * albedo.a : lml * kd * dcol;
if (overlay.w > 2.5) {
// spec/ecc/refmask at t4: phong vs the FIXED literal light
// (-0.14, 0.5, 0.9), power 10 + 290*ecc, tint (2.1, 1.8, 1.5),
// scaled by the clamped lightmap green and the spec mask.
float4 masks = decal_art.Sample(smp, i.uv);
float3 vd = -normalize(i.rpos);
float3 Ls = float3(-0.14, 0.5, 0.9);
float3 refl = Ls - 2.0 * wn * dot(wn, Ls);
float bp = saturate(dot(vd, -refl));
float ks = pow(max(bp, 1e-6), 10.0 + 290.0 * masks.y);
// misc.z = 5 (F12 reflection isolation): body only, no spec/cube.
if (abs(misc.z - 5.0) > 0.5) {
lin += ks * float3(2.1, 1.8, 1.5) * lml.g * masks.x;
}
if (((fam > 4.5 && fam < 6.5) || fam > 12.5) &&
abs(misc.z - 5.0) > 0.5 && abs(misc.z - 1.0) > 0.5) {
// Cube reflection: reflect(E, wN) with xy negated (the source's
// ref_vec.xy *= -1), luminosity lerped toward 1 by
// 0.3 * sat(4*refmask - 2.6), x refmask x reflection_scale 1.5.
float3 rv = vd - 2.0 * wn * dot(vd, wn);
// misc.y = cube LOD bias log2(render_height / 640): the guest
// computes the cube fetch's gradient LOD at its own 1152x640
// render; at 4K our per-pixel gradients are ~3.4x smaller, so
// without the bias baked cube detail (the plaza streetlight
// heads) survives through mips the console's fetch blurs away.
// F12 isolation (misc.z): mode 2 samples the ABSOLUTE level in
// misc.w; other modes add misc.w as extra bias; mode 4 shows the
// raw cube sample in place of the shaded result.
float3 dir = float3(-rv.x, -rv.y, rv.z);
// Mode 6 gives the slider to the NM fetch; the cube keeps just
// the automatic bias there.
float cube_extra = misc.z > 5.5 ? 0.0 : misc.w;
float3 cube = abs(misc.z - 2.0) < 0.5
? env_cube.SampleLevel(smp, dir, misc.w).rgb
: env_cube.SampleBias(smp, dir, misc.y + cube_extra).rgb;
float rl = 0.3 * saturate(4.0 * masks.z - 2.6);
float lum = lml.g + rl * (1.0 - lml.g);
lin += cube * lum * masks.z * 1.5;
if (abs(misc.z - 4.0) < 0.5) {
lin = cube * cube;
}
}
}
// v2 decal-family spec (spec/ecc at t9): the same phong-vs-fixed-
// light term as the base families; the decal art occupies t4 on
// fams 3/4, so their masks ride the second pair table.
if ((v2f & 4u) != 0u) {
float2 m2 = spec2_map.Sample(smp, i.uv).rg;
float3 vd2 = -normalize(i.rpos);
float3 Ls2 = float3(-0.14, 0.5, 0.9);
float3 refl2 = Ls2 - 2.0 * wn * dot(wn, Ls2);
float ks2 =
pow(max(saturate(dot(vd2, -refl2)), 1e-6), 10.0 + 290.0 * m2.y);
lin += ks2 * float3(2.1, 1.8, 1.5) * lml.g * m2.x;
}
if (fam > 12.5) {
// Fam 13 blend factor: the PS outputs a^2 (straight-alpha blend on
// top of the in-shader premultiplied body; wisps thin as ~a^3,
// same convention as transparentenvironment).
out_a = albedo.a * albedo.a;
} else if (fam > 6.5) {
out_a = albedo.a;
reduced_tone = fam > 7.5; // environmentdiffuse's cheap tonemap
}
fog_a *= sh_env.x; // material multiplier (PS c11.y)
}
// Fog -> exposure -> tonemap -> sqrt, then the postfx uber's measured
// 1.41 scene multiplier (same as the character branch).
float3 xe = (lin * fog_a + fog_rgb) * expo;
return ToneOut(xe, out_a, reduced_tone);
}
// environment.decal surfaces: the paint/graffiti art (t4) is composited
// over the base diffuse by ITS alpha, opaque output; these meshes ARE
// the wall/ground there. The art maps with uv3, the second half-pair of
// the packed half4 first texcoord (validated offline: sampling with the
// tiling uv0 repeats it: "Stereo Stereo Stereo"; the fmt-26 second
// element is the lightmap unwrap, not the decal's). Composited BEFORE the
// macro overlay: the weathering applies over the paint too
// (ApplyOverlay(cOverlay, ApplyDecal(...)); the old order left the paint
// unweathered, the too-white ramp stencils).
// Decals-&-grime build-up layer (bit 256): art and macro weathering both
// render neutral until revealed.
bool sc_decals = ShowcaseLayerOn(i.pos.x, 256);
if (overlay.w > 0.0 && sc_decals) {
// overlay.w == 2 marks environment.decal_tileable: the art tiles across
// the surface (rock/cliff faces) and must WRAP; clamp stretched the
// border texels into giant streaks. Single placements clamp (their
// transparent border keeps everything outside the placement clear).
float4 dk = overlay.w > 1.5 ? decal_art.Sample(smp, i.uv3)
: decal_art.Sample(smp_clamp, i.uv3);
albedo.rgb = lerp(albedo.rgb, dk.rgb, dk.a);
}
// Macro overlay: large-scale grime/cracks multiplied over the diffuse at
// uv * macroOverlayUVScale, faded by macroOverlayOpacity: the ground and
// wall weathering. WHITE is the neutral (materials without weathering
// bind a 16x16 "default_white"). The game multiplies it ONCE in its
// linear (squared) color space, so the gamma-space equivalent is
// sqrt(m); a direct multiply doubles the darkening (harsh black
// patchwork vs the emulated subtle weathering).
if (overlay.z > 0.0 && misc.x < 1.5 && // water reuses overlay.z (ripple map flag)
sc_decals) {
float4 m = macro.Sample(smp, i.uv * overlay.x);
albedo.rgb *= lerp(float3(1.0, 1.0, 1.0), sqrt(m.rgb), overlay.y * m.a);
}
// tint.r > 0 marks items with a lightmap bound (2x baked lighting);
// otherwise fall back to derivative face shading. The lighting term stays
// separate from the albedo so the CSM receive below can min-clamp IT, the
// way the game's GetShadowedLightMap clamps the lightmap lighting.
float3 light;
if (tint.b > 0.0) {
light = float3(1.0, 1.0, 1.0); // unlit (sky dome)
} else if (tint.r > 0.0) {
// Console lightmap semantics: bilinear, clamped, mip 0; the composed
// atlas pages are single-level; mip/wrap sampling bled neighbor cells
// and page edges (see the exact env fetch).
light = lightmap.SampleLevel(smp_clamp, i.uv2, 0.0).rgb * 2.0;
} else {
// Smooth per-vertex normal when the mesh has one; face normal from
// position derivatives otherwise.
float3 n = dot(i.nrm, i.nrm) > 0.01 ? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
light = abs(dot(n, normalize(float3(0.4, 0.8, 0.3)))) * 0.35 + 0.75;
}
// Dynamic CSM shadow receive (world geometry + rigid props; characters
// need the game's separate PCF/bias variant; skipping them avoids
// self-shadow acne, and the ground shadow is 95% of the visible effect).
// Exact receiver math from the baseenvironment PS disassembly: finest
// cascade whose |ls| < 0.99 wins; shadow = saturate(infront + 1 -
// coverage), then the game min-clamps the LINEAR lighting term:
// light_linear = min(light_linear, s + c8.rgb)
// Full shadow clamps to the dim bluish c8 ambient, the penumbra saturates
// wherever the clamp exceeds the lit level (which is what keeps the edge
// crisp), and surfaces already darker than the clamp, baked shade under
// bridges/trees, show NO dynamic shadow at all. Our light term is
// gamma-space (light^2 ~ the game's linear term: the lightmap x2 folds
// its x4 linear multiplier), so the clamp maps to min(light, sqrt(s+c8))
// per channel. A fixed-denominator curve here read pitch-black and
// double-darkened baked shade.
if (sh_misc.y > 0.0 && tint.g == 0.0 && tint.b == 0.0 && misc.x == 0.0) {
// Uncovered positions return 1 and the min-clamp is a no-op there.
float s = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, 0.0, i.nrm, i.pos.xy),
SampleStaticSun(i.rpos + cam_pos.xyz, i.nrm, i.pos.xy));
light = min(light, sqrt(s + sh_color.rgb));
}
float3 lit = albedo.rgb * light;
if (mat_tint.w > 0.0 && misc.x == 0.0) {
lit *= mat_tint.rgb;
}
if (misc.x > 1.5) {
// water.* (flowingwater.fx approximation): the real shader is
// near-black diffuse + dual time-scrolled ripple-normal taps + an
// environment-cube reflection + sun specular. We have no cube map
// bound, so the reflection term is the frame fog color (the best
// single approximation of the surroundings' haze tone) scaled by a
// fresnel curve; the ripple normal perturbs both the fresnel and a sun
// sparkle along the captured CSM light axis. The lightmap (x2) keeps
// the baked bridge/wall shading on the surface. Calibrated against the
// canal capture (emulated mid-canal mean ~(24,28,32)/255).
float t = overlay.x;
float2 rip;
if (overlay.z > 0.0) {
// Dual scrolled taps of the material's ripple normal map (macro slot).
float2 wuv = i.uv * 6.0;
float3 n1 = macro.Sample(smp, wuv + t * float2(0.11, 0.06)).rgb;
float3 n2 = macro.Sample(smp, wuv * 1.71 - t * float2(0.07, 0.13)).rgb;
rip = (n1.xy + n2.xy) - 1.0;
} else {
// Normal map unresolved: procedural ripples from world position.
// Wavelengths ~0.4-1m (emulated ripples are decimeter-scale); low
// frequencies formed meter-wide chevron interference bands that read
// as giant arrows on the surface.
float3 wp = i.rpos + cam_pos.xyz;
rip = 0.35 * float2(sin(wp.x * 9.7 + wp.z * 6.1 + t * 2.3) +
0.6 * sin(wp.x * 17.3 - wp.z * 11.9 + t * 3.4),
cos(wp.x * 7.1 - wp.z * 13.7 + t * 2.7) +
0.6 * cos(wp.x * 12.9 + wp.z * 18.3 + t * 3.1));
}
float3 wn = normalize(float3(rip.x * 0.4, 2.0, rip.y * 0.4));
float3 vd = -normalize(i.rpos);
float fres = pow(1.0 - saturate(dot(vd, wn)), 3.0);
// The flowing-water lightmap unwrap decodes unreliably (bands across
// atlas gutters), so the water term deliberately ignores it: near-black
// body + ripple-perturbed cube reflection + sun sparkle.
// Deep body: the water "diffuse" is a faint STRIPE MASK (max 24/255,
// WaterFallFoamAlpha, a lookup for the real shader, not a color). The
// game consumes it in linear space where 0.09^2 vanishes; squaring here
// likewise kills the visible blue/black banding. overlay.w > 0 = no
// diffuse channel at all (ocean.default); body is zero there, NOT the
// white fallback (ocean.fx: diffTerm = (0,0,0), color is all reflection).
float3 col = overlay.w > 0.5 ? 0.0 : albedo.rgb * albedo.rgb * 0.6;
// Reflection tint: the environment cube when resolved (t6); otherwise a
// haze derived from the frame fog color, lifted toward neutral so dark
// dusk fog doesn't collapse the water to black (fit: emulated canal
// mean ~(24,28,32)/255 with fog color (0.02,0.07,0.13)).
float3 renv = overlay.y > 0.0
? env_cube.Sample(smp, reflect(-vd, wn)).rgb
: mat_tint.rgb * 0.5 + 0.06;
col += renv * (0.55 + 0.45 * fres);
if (sh_misc.y > 0.0) {
float3 h = normalize(vd + normalize(-sh_z.xyz));
col += pow(saturate(dot(wn, h)), 90.0) * 0.35; // sun sparkle
}
float fade = saturate(length(i.rpos) * misc.y + misc.z);
if (misc.w != 1.0) {
fade = pow(max(fade, 1e-4), misc.w);
}
col = sqrt(max(col * col * saturate(1.0 + fade * mat_tint.w) +
fade * mat_tint.rgb, 0.0));
// Opaque: the game's murk hides the canal bed entirely (and our bed
// shading is untrustworthy under water: striped lightmap unwraps).
return float4(PassGamma(col), 1.0);
}
if (misc.x > 0.0) {
// transparentenvironment.fx (Skate 2 source; disassembled Skate 3 PS
// matches): outcolor.rgb = diffTerm * diffuse.rgb * diffuse.a; the rgb
// is premultiplied by alpha ONCE IN THE SHADER on top of the a^2 blend
// factor, so wisps thin out as ~a^3. That cubic falloff is most of the
// emulated "sparse clouds" look. Fog is applied in the game's linear
// (squared) color space: fade = saturate(dist * ramp.x + ramp.y)^ramp.z
// toward the fog color, transmittance = 1 + fade * fogcolor.w.
lit *= albedo.a;
float fade = saturate(length(i.rpos) * misc.y + misc.z);
if (misc.w != 1.0) {
fade = pow(max(fade, 1e-4), misc.w);
}
lit = sqrt(max(lit * lit * saturate(1.0 + fade * mat_tint.w) +
fade * mat_tint.rgb, 0.0));
return float4(PassGamma(lit), albedo.a * albedo.a);
}
return float4(PassGamma(lit), 1.0);
}
float4 ps_main(VSOut i) : SV_Target {
int mask = ShowcaseMask(i.pos.x);
// Dynamic-entities build-up layer (bit 512): entity draws (tint.a < 0,
// staged CPU-side from DrawItem::dyn_entity) drop out entirely until the
// bit reveals on this pixel's side of the split; no color and no depth,
// so the world behind them renders as if they were never published.
if (mask >= 0 && (mask & 512) == 0 && tint.a < -0.5) {
clip(-1.0);
return float4(0.0, 0.0, 0.0, 0.0);
}
float4 c = ShadePixel(i);
// Blackout stage (bit 1024, the showcase's opening/closing bookends and
// the recording cut marker): every draw renders black, keeping its own
// alpha so coverage, alpha test and blend routing stay intact.
if (mask >= 0 && (mask & 1024) != 0) {
return float4(0.0, 0.0, 0.0, c.a);
}
if (mask < 0 || (mask & 4) != 0) {
return c; // showcase off, or the full material layer is revealed
}
// Pre-material build-up looks replace the shaded rgb but keep the
// branch's own alpha, so alpha tests, coverage channels, fades and blend
// routing all behave exactly as in the full render. The gamma-space
// looks encode through PassGamma like the legacy branches; the exact-
// chain lighting look goes through ToneOut. The sky dome (exact family
// -40 / the legacy unlit marker) is authored content; it arrives
// complete with the lighting layer and never receives cast shadows.
bool sky = (cam_pos.w < -39.5 && cam_pos.w > -40.5) || tint.b > 0.0;
if (sky && (mask & 2) != 0) {
return c;
}
float3 n = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
float3 ldir = dot(sh_sun.xyz, sh_sun.xyz) > 0.1
? sh_sun.xyz
: normalize(float3(0.4, 0.8, 0.3));
// The dynamic-shadow layer applies to every pre-material look (the
// helpers self-gate on the mask, so uncovered frames sample lit).
float s = 1.0;
if (!sky && (mask & 8) != 0) {
s = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz,
tint.g > 0.0 ? 0.012 : 0.0, i.nrm, i.pos.xy),
SampleStaticSun(i.rpos + cam_pos.xyz, i.nrm, i.pos.xy));
}
// Base color: the diffuse texture once the albedo layer is revealed,
// clay grey before it.
float3 base = (mask & 1) != 0 ? diffuse.Sample(smp, i.uv).rgb
: float3(0.62, 0.62, 0.62);
if ((mask & 2) != 0 && tint.r > 0.0 && sh_sun.w > 0.01) {
// Baked lighting on a lightmapped surface: the REAL diffuse chain,
// lightmap^2 x the flat-map kd x fog/exposure/material multiplier,
// the exact-family formula minus surface detail, so the later step
// to full materials only adds normal/detail maps, specular,
// reflections and weathering instead of also re-grading the frame's
// tone. The shadow layer joins with the game's own lightmap min-clamp.
float3 lm = lightmap.SampleLevel(smp_clamp, i.uv2, 0.0).rgb;
float3 lml = lm * lm;
if ((mask & 8) != 0) {
lml = min(lml, s + sh_color.rgb);
}
float3 lin = lml * 0.93429 * base * base;
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 xe = (lin * ((1.0 + sh_fogc.a * f1) * sh_env.x) +
sh_fogc.rgb * f1) * sh_sun.w;
return ToneOut(xe, c.a, false);
}
float3 col;
if ((mask & 2) != 0) {
// Lighting without exact frame rows: the legacy-branch convention
// (lightmap x2 in gamma space), or a neutral wrapped lambert for
// surfaces without a lightmap (characters, props).
float3 lite;
if (tint.r > 0.0) {
lite = lightmap.SampleLevel(smp_clamp, i.uv2, 0.0).rgb * 2.0;
} else {
float wrap = 0.35 + 0.65 * saturate(dot(n, ldir));
lite = float3(wrap, wrap, wrap);
}
col = base * lite;
} else if ((mask & 1) != 0) {
col = base; // raw diffuse, unlit
} else {
// Clay: grey with a simple sun-lambert so the geometry reads (a fully
// flat grey collapses everything into silhouettes).
col = base * (0.61 + 0.68 * saturate(dot(n, ldir)));
}
// Shadow layer on the gamma-space looks: a soft darkening multiply (the
// exact-chain look above uses the game's own clamp instead).
if (!sky && (mask & 8) != 0) {
col *= s * 0.65 + 0.35;
}
return float4(PassGamma(saturate(col)), c.a);
}
// ---- SSR reflection G-buffer (consumed by ssr.hlsl ps_march) --------------
// 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:
// RG = octahedral WORLD-space normal, B = linear view depth (the clip w),
// A = reflectivity weight, negated for surfaces that never write scene
// depth (water / blended fam-13 glass), which selects the march's in-front
// visibility rule instead of the exact depth match. The caller re-stages
// each item's main-pass root constants and t3/t4/t5 textures, so the normal
// composition below is the material branch's own math.
float2 OctEncode(float3 n) {
n /= abs(n.x) + abs(n.y) + abs(n.z);
if (n.z < 0.0) {
n.xy = float2((1.0 - abs(n.y)) * (n.x >= 0.0 ? 1.0 : -1.0),
(1.0 - abs(n.x)) * (n.y >= 0.0 ? 1.0 : -1.0));
}
return n.xy;
}
float4 ps_refl_gbuf(VSOut i) : SV_Target {
float3 wn;
float w;
float trans;
// Env families carry cam_pos.w = -fam; water rides the legacy branch with
// cam_pos.w = 0 (misc.x is the packed normal-trim on fams 5/6 here, so it
// cannot discriminate).
if (cam_pos.w < -30.5 && cam_pos.w > -32.5) {
// Exact ocean families: no SSR contribution (the ocean keeps its own
// cube/baked reflections): zero weight, in-front visibility.
return float4(OctEncode(float3(0.0, 1.0, 0.0)), i.pos.w, -0.0);
}
if (cam_pos.w < -29.5 && cam_pos.w > -30.5) {
// Exact water (fam 30): the branch's own ripple-perturbed normal;
// weight from the thresholded reflection mask (t4.z), boosted like the
// glass arm below. Never writes scene depth -> in-front visibility.
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;
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)));
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 wwn = normalize(vn.x * wt + vn.y * wb + vn.z * wn0);
float wmask = overlay.x > 0.0
? saturate(decal_art.Sample(smp, i.uv).z - wat_p3.y)
: 0.0;
return float4(OctEncode(wwn), i.pos.w, -saturate(wmask * 2.5));
}
if (cam_pos.w < -0.5) {
// Fams 5/6/13: the reflective branch's own normal + reflection mask.
// No alpha test; masked reflective draws skip the clip in ps_main too
// (overlay.w >= 3).
float fam = -cam_pos.w;
float4 masks = decal_art.Sample(smp, i.uv);
// Blend weight, not the material's energy term: the raw refmask
// (~0.2-0.5 on glass) is authored to scale the cube ADDEND; using it
// directly as the SSR lerp weight leaves the cube fallback dominating
// and the reflections imperceptible. Boost so real glass approaches
// full replacement while zero-mask texels stay out.
w = saturate(masks.z * 2.5);
trans = fam > 12.5 ? 1.0 : 0.0;
wn = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
if (overlay.w > 3.5) {
// Normal-mapped per-panel tilts (fams 5/6): the same composition as
// the material branch (t5 map + the constant detail-fold trim packed
// in misc.x; analytic world-up axes carrying the screen-frame signs).
float3 tt, bb;
ScreenTangentFrame(wn, i.rpos, i.uv, tt, bb);
float3 bb2 = float3(0.0, 1.0, 0.0) - wn * wn.y;
float lb2 = length(bb2);
if (lb2 > 0.05) {
bb2 /= lb2;
float3 tt2 = cross(bb2, wn);
tt = tt2 * (dot(tt2, tt) >= 0.0 ? 1.0 : -1.0);
bb = bb2 * (dot(bb2, bb) >= 0.0 ? 1.0 : -1.0);
}
float trim_yi = floor(misc.x / 1000.0);
float2 trim = float2(misc.x - trim_yi * 1000.0 - 500.0,
trim_yi - 500.0) * 0.001;
float3 nmv = normal_map.Sample(smp, i.uv).rgb;
float3 nt = float3(nmv.xy * 2.0 - 1.0 + trim, nmv.z * 2.0 - 1.0);
wn = normalize(nt.x * tt + nt.y * bb + wn * max(nt.z, 0.05));
}
} else {
// Water: the water branch's ripple-perturbed up normal; reflectivity =
// the branch's own fresnel-scaled reflection weight (0.55 + 0.45*fres).
float t = overlay.x;
float2 rip;
if (overlay.z > 0.0) {
float2 wuv = i.uv * 6.0;
float3 n1 = macro.Sample(smp, wuv + t * float2(0.11, 0.06)).rgb;
float3 n2 = macro.Sample(smp, wuv * 1.71 - t * float2(0.07, 0.13)).rgb;
rip = (n1.xy + n2.xy) - 1.0;
} else {
float3 wp = i.rpos + cam_pos.xyz;
rip = 0.35 * float2(sin(wp.x * 9.7 + wp.z * 6.1 + t * 2.3) +
0.6 * sin(wp.x * 17.3 - wp.z * 11.9 + t * 3.4),
cos(wp.x * 7.1 - wp.z * 13.7 + t * 2.7) +
0.6 * cos(wp.x * 12.9 + wp.z * 18.3 + t * 3.1));
}
wn = normalize(float3(rip.x * 0.4, 2.0, rip.y * 0.4));
float3 vd = -normalize(i.rpos);
float fres = pow(1.0 - saturate(dot(vd, wn)), 3.0);
w = 0.55 + 0.45 * fres;
trans = 1.0;
}
// B = the view depth in meters. SV_Position.w here delivers clip-space w
// directly, which equals view Z under the row-vector projection (m23 = 1)
// - verified empirically: marches from ViewPos(uv, pos.w) produce
// geometrically coherent reflections, and inverting this value collapses
// every downstream depth test (all rays die at the visibility gate).
return float4(OctEncode(wn), i.pos.w, w * (trans > 0.5 ? -1.0 : 1.0));
}
// Shadow caster pass: vs_main runs with mvp = (world *) lightVP built from
// the captured receiver rows, so SV_Position.z IS the light-space depth
// (the height-ramp row; viewport z range 0..1, DepthClip off so casters
// outside the 12 m depth window clamp like the game accepts). MIN blend on
// both channels against a (1, 1) clear: R accumulates the min depth, G
// drops to 0 where any caster drew ("uncoverage"; the blur pass converts
// to the game's coverage convention).
float2 ps_shadow_caster(VSOut i) : SV_Target {
return float2(i.pos.z, 0.0);
}
// Alpha-tested caster variant (foliage cards, alphatest fences/grates in
// the static sun-shadow map; dynamicobject.alphatest props in the CSM
// atlas): sampling the item's diffuse keeps the cutout silhouette instead
// of casting the full card quad. ALPHAREF 30/255 matches the world
// families' own opaque alpha test.
float2 ps_shadow_caster_clip(VSOut i) : SV_Target {
// +2 LOD bias: at the static sun map's ~2 cm near-cascade texels the
// raw leaf cutout rasters as binary salt-and-pepper dapple; two mip
// levels of alpha pre-filtering merge it into the soft blobs the baked
// lighting shows. The low ALPHAREF keeps mip-averaged coverage from
// eroding the canopy.
clip(diffuse.SampleBias(smp, i.uv, 2.0).a - 0.1176);
return float2(i.pos.z, 0.0);
}