// 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 diffuse : register(t0); Texture2D lightmap : register(t1); Texture2D macro : register(t3); Texture2D 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 normal_map : register(t5); Texture2D shadow_atlas : register(t7); TextureCube 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 detail_map : register(t8); Texture2D 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 static_sun : register(t10); // Raw bone palette: 3 float4 rows per bone, column-vector affine [R | t], // applied with explicit dots (StructuredBuffer default packing is // column-major and would silently transpose the matrices). StructuredBuffer 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); }