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