render: move HLSL to source files, diagnostics TU, staged RenderScene
This commit is contained in:
parent
df6156d14e
commit
406f47c79c
14 changed files with 3448 additions and 2973 deletions
|
|
@ -227,6 +227,8 @@ set(SKATE3_COMMON_SOURCES
|
|||
src/skate3_demo_path.cpp
|
||||
src/skate3_fov.cpp
|
||||
src/skate3_iso_installer.cpp
|
||||
src/native/skate3_native_diagnostics.cpp
|
||||
src/native/skate3_native_guest_read.cpp
|
||||
src/skate3_native_debug_dialog.cpp
|
||||
src/skate3_native_render.cpp
|
||||
src/skate3_native_scene.cpp
|
||||
|
|
@ -264,6 +266,29 @@ target_include_directories(skate3 PRIVATE
|
|||
${CMAKE_CURRENT_BINARY_DIR}/include
|
||||
${REXSDK_DIR}/thirdparty/xxHash
|
||||
)
|
||||
|
||||
set(SKATE3_NATIVE_SHADER_SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/scene.hlsl
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/shadow_blur.hlsl
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/resolve.hlsl
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/blur.hlsl
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/outline.hlsl
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/overlay2d.hlsl
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/spline.hlsl
|
||||
)
|
||||
set(SKATE3_NATIVE_SHADER_HEADER
|
||||
${CMAKE_CURRENT_BINARY_DIR}/include/skate3_native_shaders.h)
|
||||
add_custom_command(
|
||||
OUTPUT ${SKATE3_NATIVE_SHADER_HEADER}
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DSHADER_DIR=${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders
|
||||
-DOUT=${SKATE3_NATIVE_SHADER_HEADER}
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/EmbedShaders.cmake
|
||||
DEPENDS ${SKATE3_NATIVE_SHADER_SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/cmake/EmbedShaders.cmake
|
||||
COMMENT "Embedding native renderer shaders"
|
||||
VERBATIM)
|
||||
target_sources(skate3 PRIVATE ${SKATE3_NATIVE_SHADER_HEADER})
|
||||
target_compile_definitions(skate3 PRIVATE SKATE3_BUILD_CONFIG="$<CONFIG>")
|
||||
if(NOT SKATE3_EFFECTIVE_TITLE_UPDATE_PACKAGE STREQUAL "")
|
||||
target_compile_definitions(skate3 PRIVATE SKATE3_HAS_TITLE_UPDATE=1)
|
||||
|
|
|
|||
41
cmake/EmbedShaders.cmake
Normal file
41
cmake/EmbedShaders.cmake
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Embeds the native renderer HLSL sources (src/native/shaders/*.hlsl) into a
|
||||
# generated C++ header so the exe stays self-contained. Run as a script:
|
||||
# cmake -DSHADER_DIR=<dir> -DOUT=<header> -P EmbedShaders.cmake
|
||||
# Each shader becomes `inline constexpr char <var>[] = R"__hlsl__(...)__hlsl__";`
|
||||
# with a leading newline, matching the original in-cpp raw string literals
|
||||
# (consumers use sizeof(kX) - 1).
|
||||
|
||||
set(SHADER_VARS
|
||||
kShaderSource
|
||||
kShadowBlurSource
|
||||
kResolveShaderSource
|
||||
kBlurShaderSource
|
||||
kOutlineShaderSource
|
||||
kShader2dSource
|
||||
kShaderSplineSource
|
||||
)
|
||||
set(SHADER_FILES
|
||||
scene.hlsl
|
||||
shadow_blur.hlsl
|
||||
resolve.hlsl
|
||||
blur.hlsl
|
||||
outline.hlsl
|
||||
overlay2d.hlsl
|
||||
spline.hlsl
|
||||
)
|
||||
|
||||
set(content "// Generated by cmake/EmbedShaders.cmake from src/native/shaders/*.hlsl.\n")
|
||||
string(APPEND content "// DO NOT EDIT - edit the .hlsl files instead.\n")
|
||||
string(APPEND content "#pragma once\n\n")
|
||||
|
||||
foreach(var file IN ZIP_LISTS SHADER_VARS SHADER_FILES)
|
||||
file(READ "${SHADER_DIR}/${file}" src)
|
||||
string(FIND "${src}" ")__hlsl__" collision)
|
||||
if(NOT collision EQUAL -1)
|
||||
message(FATAL_ERROR "${file} contains the raw-string delimiter )__hlsl__")
|
||||
endif()
|
||||
string(APPEND content "// ${file}\n")
|
||||
string(APPEND content "inline constexpr char ${var}[] = R\"__hlsl__(\n${src})__hlsl__\";\n\n")
|
||||
endforeach()
|
||||
|
||||
file(WRITE "${OUT}" "${content}")
|
||||
59
src/native/shaders/blur.hlsl
Normal file
59
src/native/shaders/blur.hlsl
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Popup background blur: EXACT port of the game's dedicated pass chain
|
||||
// (captured from a live frame with the Rewards popup up):
|
||||
// blur_hBlurPS: 11 gaussian taps along +X over the finished frame,
|
||||
// offsets k * 0.0003125 * PS c0.x (c0.x = 8 -> 0.0025/tap,
|
||||
// +/-1.25% of the buffer), literal weights below (sum 1);
|
||||
// blur_vBlurPS: the same kernel along +Y;
|
||||
// postfx_basictex: plain fullscreen REPLACE of the frame with the result.
|
||||
// The game runs it at its fixed 1152x640 internal resolution and the
|
||||
// console's bilinear upscale of that buffer is what reads as the "frosted
|
||||
// glass" lattice, so the passes here render into 1152x640 intermediates and
|
||||
// stretch back, reproducing both the kernel and the lattice.
|
||||
cbuffer C : register(b0) {
|
||||
float4 dir; // xy = blur axis, z = kernel scale (the game's PS c0.x, 8)
|
||||
};
|
||||
Texture2D<float4> src : register(t0);
|
||||
SamplerState smp_clamp : register(s1);
|
||||
struct VSOut {
|
||||
float4 pos : SV_Position;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
VSOut vs_main(uint id : SV_VertexID) {
|
||||
VSOut o;
|
||||
float2 uv = float2((id << 1) & 2, id & 2);
|
||||
o.pos = float4(uv * float2(2, -2) + float2(-1, 1), 0, 1);
|
||||
o.uv = uv;
|
||||
return o;
|
||||
}
|
||||
float4 ps_main(VSOut i) : SV_Target {
|
||||
// Literal kernel from the blur_hBlurPS ucode (c251..c255).
|
||||
const float w[6] = {0.2005654, 0.1769984, 0.1216491, 0.0651141,
|
||||
0.0271436, 0.0088122};
|
||||
float2 step = dir.xy * (0.0003125 * dir.z);
|
||||
float3 c = src.SampleLevel(smp_clamp, i.uv, 0).rgb * w[0];
|
||||
[unroll] for (int k = 1; k < 6; ++k) {
|
||||
c += src.SampleLevel(smp_clamp, i.uv + step * k, 0).rgb * w[k];
|
||||
c += src.SampleLevel(smp_clamp, i.uv - step * k, 0).rgb * w[k];
|
||||
}
|
||||
return float4(c, 1.0);
|
||||
}
|
||||
// Prefiltered downsample of the (higher-res) native output into the game's
|
||||
// 1152x640 blur space (dir.xy = source texel size). A 4x4 grid of bilinear
|
||||
// taps covers reduction ratios up to ~4x (4K -> 1152 is 3.33x); narrower
|
||||
// footprints undersampled the scale and the aliased detail crawled as the
|
||||
// scene animated: the whole blurred backdrop shimmered, worst around
|
||||
// high-contrast edges.
|
||||
float4 ps_down(VSOut i) : SV_Target {
|
||||
float3 c = 0.0;
|
||||
[unroll] for (int y = 0; y < 4; ++y) {
|
||||
[unroll] for (int x = 0; x < 4; ++x) {
|
||||
float2 off = float2(float(x) - 1.5, float(y) - 1.5) * dir.xy;
|
||||
c += src.SampleLevel(smp_clamp, i.uv + off, 0).rgb;
|
||||
}
|
||||
}
|
||||
return float4(c / 16.0, 1.0);
|
||||
}
|
||||
// postfx_basictex: oC0 = tex (verified: `max oC0, r0, r0`).
|
||||
float4 ps_blit(VSOut i) : SV_Target {
|
||||
return float4(src.SampleLevel(smp_clamp, i.uv, 0).rgb, 1.0);
|
||||
}
|
||||
46
src/native/shaders/outline.hlsl
Normal file
46
src/native/shaders/outline.hlsl
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// Selected-object outline composite (park editor / object mover): port of
|
||||
// postfx_edgedetectstencilPS. The game stencil-marks the selected object,
|
||||
// resolves stencil to a 576x320 texture and adds |threshold crossings| x
|
||||
// the outline color (PS c0, the editor blue) onto the frame; our mask pass
|
||||
// renders the selected items into a small R8 target instead (see
|
||||
// pso_outline_mask) and this pass composites additively over the resolved
|
||||
// output. Tap offsets are 1/576 x 1/320 UV fractions like the original, so
|
||||
// the contour thickness matches at any output resolution.
|
||||
cbuffer C : register(b0) {
|
||||
float4 color; // rgb = outline color (guest edge-detect PS c0)
|
||||
};
|
||||
Texture2D<float4> src : register(t0);
|
||||
SamplerState smp_clamp : register(s1);
|
||||
struct VSOut {
|
||||
float4 pos : SV_Position;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
VSOut vs_main(uint id : SV_VertexID) {
|
||||
VSOut o;
|
||||
float2 uv = float2((id << 1) & 2, id & 2);
|
||||
o.pos = float4(uv * float2(2, -2) + float2(-1, 1), 0, 1);
|
||||
o.uv = uv;
|
||||
return o;
|
||||
}
|
||||
float4 ps_main(VSOut i) : SV_Target {
|
||||
// Averaged 5x5 neighborhood of the (binary, full-resolution) mask with a
|
||||
// fixed UV-fraction pitch -> a smooth 0..1 ramp across the silhouette
|
||||
// boundary; the peaked profile 4m(1-m) turns the ramp into an
|
||||
// antialiased line of constant screen-fraction width. The core is
|
||||
// whitened (the emulated line goes through the game's uber grade +
|
||||
// bloom, which lifts its center toward white). A literal port:
|
||||
// binarized crossing counts on the 576x320 grid, stairstepped visibly
|
||||
// at native output resolutions.
|
||||
const float2 t = float2(0.8 / 1152.0, 0.8 / 640.0);
|
||||
float m = 0.0;
|
||||
[unroll] for (int y = -2; y <= 2; ++y) {
|
||||
[unroll] for (int x = -2; x <= 2; ++x) {
|
||||
m += src.SampleLevel(smp_clamp, i.uv + float2(x, y) * t, 0).r;
|
||||
}
|
||||
}
|
||||
m /= 25.0;
|
||||
float band = saturate(6.4 * m * (1.0 - m));
|
||||
float core = band * band * band;
|
||||
// Additive blend (ONE, ONE) onto the frame; alpha untouched (ZERO, ONE).
|
||||
return float4(color.rgb * band + 0.5 * core, 0.0);
|
||||
}
|
||||
36
src/native/shaders/overlay2d.hlsl
Normal file
36
src/native/shaders/overlay2d.hlsl
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// 2D/APT overlay shader. Verified against a captured HUD draw stream:
|
||||
// vertices are {float4 pos, float2 uv} in 1280x720
|
||||
// APT movie space; the game's VS constants apply as clip = ortho * (world *
|
||||
// pos) with c0..c3 the ortho rows, c4..c7 the element's transform rows and
|
||||
// c8 the color multiplier, used exactly as staged.
|
||||
cbuffer C : register(b0) {
|
||||
float4 m[10]; // m[0..3] proj rows, m[4..7] world rows, m[8] color,
|
||||
// m[9].x = apply D3D9 half-pixel (2D ortho draws only)
|
||||
};
|
||||
Texture2D<float4> tex : register(t0);
|
||||
SamplerState smp : register(s1);
|
||||
struct VSOut {
|
||||
float4 pos : SV_Position;
|
||||
float2 uv : TEXCOORD0;
|
||||
float4 color : COLOR0;
|
||||
};
|
||||
VSOut vs_main(float4 p : POSITION, float2 uv : TEXCOORD0, float4 color : COLOR0) {
|
||||
float4 wp = float4(dot(p, m[4]), dot(p, m[5]), dot(p, m[6]), dot(p, m[7]));
|
||||
VSOut o;
|
||||
o.pos = float4(dot(wp, m[0]), dot(wp, m[1]), dot(wp, m[2]), dot(wp, m[3]));
|
||||
// D3D9 half-pixel convention: the art bakes half-texel UVs expecting
|
||||
// pixel centers at integer coordinates; without this the clock-face
|
||||
// quadrant tiles show their wrapped border rows as dark seam lines.
|
||||
// Scaled for the 2D ortho; must not apply to 3D (world-space
|
||||
// SimpleDraw markers), where m[0].x is a projection scale.
|
||||
if (m[9].x > 0.0) {
|
||||
o.pos.x -= 0.5 * m[0].x * o.pos.w;
|
||||
o.pos.y -= 0.5 * m[1].y * o.pos.w;
|
||||
}
|
||||
o.uv = uv;
|
||||
o.color = color;
|
||||
return o;
|
||||
}
|
||||
float4 ps_main(VSOut i) : SV_Target {
|
||||
return tex.Sample(smp, i.uv) * m[8] * i.color;
|
||||
}
|
||||
16
src/native/shaders/resolve.hlsl
Normal file
16
src/native/shaders/resolve.hlsl
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Fullscreen MSAA resolve: the deferred command list has no
|
||||
// ResolveSubresource, so average the samples in a pixel shader (SAMPLES is
|
||||
// injected as a compile-time macro).
|
||||
Texture2DMS<float4> src : register(t0);
|
||||
float4 vs_main(uint id : SV_VertexID) : SV_Position {
|
||||
float2 uv = float2((id << 1) & 2, id & 2);
|
||||
return float4(uv * float2(2, -2) + float2(-1, 1), 0, 1);
|
||||
}
|
||||
float4 ps_main(float4 pos : SV_Position) : SV_Target {
|
||||
int2 p = int2(pos.xy);
|
||||
float4 c = 0;
|
||||
[unroll] for (int k = 0; k < SAMPLES; ++k) {
|
||||
c += src.Load(p, k);
|
||||
}
|
||||
return c / SAMPLES;
|
||||
}
|
||||
816
src/native/shaders/scene.hlsl
Normal file
816
src/native/shaders/scene.hlsl
Normal file
|
|
@ -0,0 +1,816 @@
|
|||
// Face-normal shading uses the camera-relative world position: interpolating
|
||||
// absolute world coordinates (hundreds of meters) destroys ddx/ddy precision
|
||||
// and produces per-pixel noise.
|
||||
cbuffer C : register(b0) {
|
||||
row_major float4x4 world;
|
||||
row_major float4x4 mvp;
|
||||
float4 tint;
|
||||
float4 cam_pos;
|
||||
// Per-material color multiplier (w > 0 enables): CAS hair is a grayscale
|
||||
// texture times the character's hair color.
|
||||
float4 mat_tint;
|
||||
// x = macroOverlayUVScale, y = macroOverlayOpacity, z > 0 = macro overlay
|
||||
// texture bound at t3, w > 0 = environment.decal item with its `decal`
|
||||
// art bound at t4 (composited in-shader over the diffuse, exactly like
|
||||
// the game's decalenvironment PS: lerp by the art's alpha; opaque).
|
||||
// Macro and decal art are INDEPENDENT: decal ground/wall sections carry
|
||||
// the same macrooverlay as their non-decal neighbors, sharing one slot
|
||||
// dropped the macro grime there and rendered alternating plaza sections
|
||||
// ~1.4x too bright (the large-scale ground checkerboard).
|
||||
float4 overlay;
|
||||
// x > 0 = environment.transparent item (alpha-blended sub-pass): shading
|
||||
// follows the game's transparentenvironment.fx; the opaque pass's
|
||||
// alpha-test turned the soft mist gradients into solid white cloud blobs.
|
||||
// yzw = the global distance-fog RAMP (scale/bias/exponent from main-pass
|
||||
// VS c5), and mat_tint doubles as the linear-space fog COLOR (rgb +
|
||||
// transmittance scale in w, VS c6) for transparent items; the root
|
||||
// signature is capped at 64 DWORDs, so fog rides in slots transparent
|
||||
// items never use otherwise. Fog is currently applied to transparent
|
||||
// items only (the km-distant mist sheets; everything else we render is
|
||||
// near enough for fog to be negligible).
|
||||
float4 misc;
|
||||
};
|
||||
// Per-frame dynamic-shadow (CSM) receiver constants, captured from the
|
||||
// game's world-material PIXEL banks.
|
||||
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
|
||||
// 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-family lighting (defaultcharacter.fx and friends): canonical
|
||||
// per-draw rows captured from the guest PIXEL constant bank at palette
|
||||
// capture (CaptureCharLighting has the per-family register maps; the math
|
||||
// below was validated offline by executing the game's own pixel shaders).
|
||||
// Enabled per draw via cam_pos.w = family (0 = not a character / capture
|
||||
// failed -> the legacy empirical shading below).
|
||||
cbuffer CH : register(b2) {
|
||||
float4 ch_light; // xyz = sun direction, w = hair fresnel power
|
||||
float4 ch_key; // rgb = key (sun) color, w = exposure
|
||||
float4 ch_amb; // rgb = flat ambient, w = SH ambient multiplier / hair ambient
|
||||
float4 ch_sh[9]; // SH irradiance rows, pre-scaled (see capture);
|
||||
// vehicles keep spec color + power in row 0 instead
|
||||
float4 ch_tintA; // CAC diffuse tint / livingworld red-mask tint (w = apply)
|
||||
float4 ch_tintB; // livingworld blue-mask tint / hair fresnel tint (w = strand-alpha scale)
|
||||
float4 ch_misc; // x = alpha out, y = family
|
||||
};
|
||||
Texture2D<float4> diffuse : register(t0);
|
||||
Texture2D<float4> lightmap : register(t1);
|
||||
Texture2D<float4> macro : register(t3);
|
||||
Texture2D<float4> decal_art : register(t4);
|
||||
// Paired second descriptor of the t4 table: the reflective families'
|
||||
// (fam 5/6) normal map. Only valid, and only sampled, when overlay.w == 4.
|
||||
Texture2D<float4> normal_map : register(t5);
|
||||
Texture2D<float2> shadow_atlas : register(t7);
|
||||
TextureCube<float4> env_cube : register(t6);
|
||||
// Raw bone palette: 3 float4 rows per bone, column-vector affine [R | t],
|
||||
// applied with explicit dots (StructuredBuffer<float4x4> default packing is
|
||||
// column-major and would silently transpose the matrices).
|
||||
StructuredBuffer<float4> bones : register(t2);
|
||||
SamplerState smp : register(s0);
|
||||
// s1 = bilinear CLAMP (shared with the 2D pass). Decal art must clamp: the
|
||||
// art UV runs far outside [0,1] across big ground sheets and the art's
|
||||
// transparent border keeps everything outside the single placement clear;
|
||||
// wrap sampling tiled the graffiti across the whole plaza.
|
||||
SamplerState smp_clamp : register(s1);
|
||||
struct VSOut {
|
||||
float4 pos : SV_Position;
|
||||
float3 rpos : TEXCOORD0;
|
||||
float2 uv : TEXCOORD1;
|
||||
float2 uv2 : TEXCOORD2;
|
||||
float3 nrm : TEXCOORD3;
|
||||
float2 uv3 : TEXCOORD4;
|
||||
};
|
||||
VSOut vs_main(float3 p : POSITION, float2 uv : TEXCOORD0, float2 uv2 : TEXCOORD1,
|
||||
float4 bw : BLENDWEIGHT0, uint4 bi : BLENDINDICES0,
|
||||
float3 nrm : NORMAL0, float2 uv3 : TEXCOORD2) {
|
||||
VSOut o;
|
||||
float4 mp = float4(p, 1.0);
|
||||
float3 n = nrm;
|
||||
// tint.g > 0 marks a skinned item: the bone palette (row-vector matrices)
|
||||
// maps model space to world space; mvp is then just view*proj.
|
||||
float wsum = dot(bw, float4(1, 1, 1, 1));
|
||||
if (tint.g > 0.0 && wsum > 0.001) {
|
||||
float3 skinned = float3(0, 0, 0);
|
||||
float3 sn = float3(0, 0, 0);
|
||||
// Guest blend indices are plain bone numbers (verified live: byte
|
||||
// streams like 02 00 03 01); bone k = palette rows 3k..3k+2.
|
||||
[unroll] for (int k = 0; k < 4; ++k) {
|
||||
uint row = bi[k] * 3u;
|
||||
skinned += bw[k] * float3(dot(mp, bones[row]), dot(mp, bones[row + 1]),
|
||||
dot(mp, bones[row + 2]));
|
||||
sn += bw[k] * float3(dot(n, bones[row].xyz), dot(n, bones[row + 1].xyz),
|
||||
dot(n, bones[row + 2].xyz));
|
||||
}
|
||||
mp = float4(skinned / wsum, 1.0);
|
||||
n = sn;
|
||||
} else {
|
||||
n = mul(n, (float3x3)world);
|
||||
}
|
||||
o.pos = mul(mp, mvp);
|
||||
o.rpos = mul(mp, world).xyz - cam_pos.xyz;
|
||||
o.uv = uv;
|
||||
o.uv2 = uv2;
|
||||
o.nrm = n;
|
||||
o.uv3 = uv3;
|
||||
return o;
|
||||
}
|
||||
// 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). 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;
|
||||
}
|
||||
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 suv = float2(luv.x / 6.0 + (casc * 2.0 - 1.0) / 6.0,
|
||||
luv.y * -0.5 + 0.5);
|
||||
float2 sm2 = shadow_atlas.Sample(smp_clamp, suv);
|
||||
return saturate((sm2.x >= rd ? 1.0 : 0.0) + (1.0 - sm2.y));
|
||||
}
|
||||
float4 ps_main(VSOut i) : SV_Target {
|
||||
if (tint.a > 0.0) {
|
||||
return tint;
|
||||
}
|
||||
float4 albedo = diffuse.Sample(smp, i.uv);
|
||||
// Alpha-tested foliage/fences; opaque formats sample alpha = 1. Character
|
||||
// diffuse packs GLOSS in alpha; never clip characters. tint.g > 0 marks
|
||||
// them: set for bones-bound skinned items AND for ropa cloth garments
|
||||
// rendered rigid (sim-active player tees, clipping their gloss alpha
|
||||
// discarded every pixel: the invisible-shirt bug; their decode writes
|
||||
// zero blend weights, so the VS skinning branch stays off).
|
||||
if (tint.g == 0.0 && overlay.w < 0.5 && cam_pos.w > -20.5) {
|
||||
// environment.transparent alpha-tests its SQUARED alpha at ref 16/255
|
||||
// (transparentenvironment.xml: ALPHAREF 16, PS outputs a = diffuse.a^2).
|
||||
// Exact env families (cam_pos.w < 0) use the game's world ALPHAREF 30.
|
||||
// dynamicobject items (cam_pos.w <= -21) are excluded here and clip in
|
||||
// their own branch (only the .alphatest variant tests, at ALPHAREF 30).
|
||||
// Fam 13 (reflective_trans glass, cam_pos.w = -13) never alpha-tests;
|
||||
// it alpha-BLENDS in the sorted sub-pass.
|
||||
if (cam_pos.w > -12.5 || cam_pos.w < -13.5) {
|
||||
float aref = cam_pos.w < -0.5 ? 0.1176 : 0.35;
|
||||
clip(misc.x > 0.0 ? albedo.a * albedo.a - 0.0627 : albedo.a - aref);
|
||||
}
|
||||
}
|
||||
// Exact sky dome (cam_pos.w = -40; sky_defaultPS transcribed from the
|
||||
// Skate 3 ucode in a live capture). The emulated frame's big sun
|
||||
// glow is computed IN the dome shader: a 1D radial gradient (the sky
|
||||
// material's `specular` channel, bound at t4 here) indexed by the sine of
|
||||
// the angle between the dome direction and the sun, its rgb^2 amplified
|
||||
// by 1/sat(a + 0.01), the alpha falloff makes the core HDR-bright, then
|
||||
// added to the squared panorama and run through the standard exposure/
|
||||
// tonemap/sqrt chain (no fog on the sky) and the postfx uber 1.41.
|
||||
// mat_tint.xyz = sun dir, mat_tint.w = the level sky elevation;
|
||||
// overlay.x = sun angular scale, overlay.y = pre-tone multiplier,
|
||||
// misc.y = scene exposure.
|
||||
if (cam_pos.w < -39.5 && cam_pos.w > -40.5) {
|
||||
// The dome mesh is camera-relative: world = mesh + (cam.x, sky_h,
|
||||
// cam.z), so the shader's dome-local direction (sky.fx In.vPos) is
|
||||
// rpos + (0, cam.y - sky_h, 0).
|
||||
float3 dome = normalize(i.rpos + float3(0.0, cam_pos.y - mat_tint.w, 0.0));
|
||||
float3 lin = albedo.rgb * albedo.rgb;
|
||||
float dotPL = saturate(dot(dome, mat_tint.xyz));
|
||||
float sinA = sqrt(saturate(1.0 - dotPL * dotPL));
|
||||
float4 sun = decal_art.Sample(smp_clamp, float2(sinA / overlay.x, 0.5 / 16.0));
|
||||
lin += sun.rgb * sun.rgb / saturate(sun.a + 0.01);
|
||||
float3 xe = lin * overlay.y * misc.y;
|
||||
float3 t1 = saturate(1.0 - xe);
|
||||
float3 tm = max(xe * 0.25 + 0.75, 1.0) - t1 * t1;
|
||||
float3 cc = saturate(sqrt(max(tm * 0.5, 0.0)) * 1.41);
|
||||
return float4(cc, 1.0);
|
||||
}
|
||||
// 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):
|
||||
// key sun light + bounce + flat ambient, gated by
|
||||
// the CSM shadow (the static world-shadow map is approximated as fully lit
|
||||
// - its floor c8.w bounds it), then fog -> exposure -> tonemap -> sqrt and
|
||||
// the postfx uber 1.41. v1 uses the geometric normal (cross of the mesh
|
||||
// tangent/binormal) with the flat normal-map kd 0.93429; the base/detail/
|
||||
// spec normal maps are v2.
|
||||
if (cam_pos.w < -20.5) {
|
||||
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)));
|
||||
float ndl = dot(n, dyn_sun.xyz);
|
||||
float key = saturate(ndl); // key light gated on N.L >= 0
|
||||
float bounce = saturate(dot(n, float3(-dyn_sun.x, dyn_sun.y, -dyn_sun.z)));
|
||||
// CSM shadow (shared receiver rows at t5); the static world-shadow map is
|
||||
// not rendered natively, so its term is 1 and the min collapses to the
|
||||
// CSM (bounded below by the c8.w floor, dyn_misc.y). 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 = SampleCsmShadow(i.rpos + cam_pos.xyz, -1.0);
|
||||
float shadow = min(s * (ndl >= 0.0 ? 1.0 : 0.0), max(1.0, dyn_misc.y));
|
||||
float3 lighting = key * shadow + bounce * dyn_amb.w + dyn_amb.rgb;
|
||||
// GetTangentLight with the neutral (flat) normal map: 0.39 * 2.39562.
|
||||
float3 lin = lighting * 0.93429 * dlin;
|
||||
// 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;
|
||||
float3 t1 = saturate(1.0 - xe);
|
||||
float3 tm = max(xe * 0.25 + 0.75, 1.0) - t1 * t1;
|
||||
return float4(saturate(sqrt(max(tm * 0.5, 0.0)) * 1.41), 1.0);
|
||||
}
|
||||
// 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).
|
||||
if (cam_pos.w > 0.5) {
|
||||
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 = SampleCsmShadow(i.rpos + cam_pos.xyz, 0.012);
|
||||
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 = SampleCsmShadow(i.rpos + cam_pos.xyz, 0.012);
|
||||
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;
|
||||
}
|
||||
// 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);
|
||||
float3 t1 = saturate(1.0 - lin * E);
|
||||
float3 tm = max(lin * 0.25 * E + 0.75, 1.0) - t1 * t1;
|
||||
float3 cc = saturate(sqrt(max(tm * 0.5, 0.0)) * 1.41);
|
||||
return float4(cc, out_a);
|
||||
}
|
||||
// Exact world-material families (cam_pos.w = -family): hand-ported from
|
||||
// the game's own pixel shaders and verified per-pixel against them with
|
||||
// an offline ucode interpreter. All texture
|
||||
// colors linearize IN-SHADER as x^2 (the fetch signs are unsigned on every
|
||||
// world texture). Families: 1 baseenvironment, 2 defaultenvironment,
|
||||
// 3/4 decalenvironment(_tileable), 5/6 reflective(_simple), 7 alphatest,
|
||||
// 8 environmentdiffuse, 9/10 tree(animate), 11 proxyworld,
|
||||
// 12 incandescent. v1 runs with NEUTRAL normal/detail maps (kd is the
|
||||
// exact flat-map constant 0.39 * 2.39562); spec/reflection masks bind at
|
||||
// t4 (overlay.w == 3) on families without decal art.
|
||||
if (cam_pos.w < -0.5) {
|
||||
float fam = -cam_pos.w;
|
||||
float3 dlin = albedo.rgb * albedo.rgb;
|
||||
// Global distance fog (VS c5/c6, captured per frame): every world PS
|
||||
// ends with col * fog.a + fog.rgb before exposure/tonemap.
|
||||
float fdist = length(i.rpos);
|
||||
float f1 = saturate(fdist * sh_fogp.x + sh_fogp.y);
|
||||
if (sh_fogp.z != 1.0) {
|
||||
f1 = pow(max(f1, 1e-6), sh_fogp.z);
|
||||
}
|
||||
float3 fog_rgb = sh_fogc.rgb * f1;
|
||||
float fog_a = 1.0 + sh_fogc.a * f1;
|
||||
float expo = sh_sun.w;
|
||||
float3 lin;
|
||||
float out_a = 1.0;
|
||||
bool reduced_tone = false;
|
||||
if (fam > 8.5 && fam < 12.5) {
|
||||
// tree/treeanimate: D^2 * max(lm^2, floor) * scale [* tint mult];
|
||||
// proxyworld/incandescent: D^2 * scale. No shadow receive, no kd, no
|
||||
// material multiplier on the fog term.
|
||||
if (fam < 10.5) {
|
||||
float3 lmg = lightmap.Sample(smp, i.uv2).rgb;
|
||||
lin = dlin * max(lmg * lmg, sh_env.z) * sh_env.y;
|
||||
if (fam < 9.5) {
|
||||
lin *= sh_env.w;
|
||||
}
|
||||
out_a = albedo.a;
|
||||
} else {
|
||||
lin = dlin * (fam < 11.5 ? sh_fogp.w : 1.0);
|
||||
}
|
||||
} else {
|
||||
// Environment families: macro overlay (0.5-neutral, fades under decal
|
||||
// art), linear decal composite, lightmap squared and min-clamped
|
||||
// against (CSM s + shadow color), kd, phong spec vs the shader's
|
||||
// fixed literal light, cube reflection on 5/6.
|
||||
float3 ov = float3(1.0, 1.0, 1.0);
|
||||
// Fam 13 (transparentenvironmentreflective) carries no macro term.
|
||||
if (overlay.z > 0.0 && fam < 12.5) {
|
||||
float3 mo = macro.Sample(smp, i.uv * overlay.x).rgb;
|
||||
ov = saturate((mo - 0.5) * overlay.y + 0.5);
|
||||
}
|
||||
if (fam > 2.5 && fam < 4.5 && overlay.w > 0.5) {
|
||||
// overlay.w == 0 = art unresolved (white fallback alpha 1 would
|
||||
// whitewash the whole surface).
|
||||
float4 dk = overlay.w > 1.5 ? decal_art.Sample(smp, i.uv3)
|
||||
: decal_art.Sample(smp_clamp, i.uv3);
|
||||
dlin = lerp(dlin, dk.rgb * dk.rgb, dk.a);
|
||||
ov = lerp(float3(1.0, 1.0, 1.0), ov, 1.0 - dk.a);
|
||||
}
|
||||
float3 dcol = dlin * ov;
|
||||
// CSM shadow term s = sat(infront + 1 - coverage) from the native
|
||||
// atlas (same cascade select as the legacy receive path).
|
||||
float s = 1.0;
|
||||
if (sh_misc.y > 0.0) {
|
||||
float3 wp = i.rpos + cam_pos.xyz;
|
||||
float2 lsv =
|
||||
float2(dot(sh_x.xyz, wp) + sh_x.w, dot(sh_y.xyz, wp) + sh_y.w);
|
||||
float rd = dot(sh_z.xyz, wp) + sh_z.w - sh_misc.x;
|
||||
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) {
|
||||
float2 suv = float2(luv.x / 6.0 + (casc * 2.0 - 1.0) / 6.0,
|
||||
luv.y * -0.5 + 0.5);
|
||||
float2 sm2 = shadow_atlas.Sample(smp_clamp, suv);
|
||||
s = saturate((sm2.x >= rd ? 1.0 : 0.0) + (1.0 - sm2.y));
|
||||
}
|
||||
}
|
||||
// Lightmap fetch = the console's semantics: BILINEAR, CLAMPED, mip 0.
|
||||
// The composed atlas pages are single-level on console and the fetch
|
||||
// constants carry clamp_x/clamp_y = 2. Sampling them with the aniso-8
|
||||
// WRAP sampler over our generated mip chain averaged NEIGHBORING
|
||||
// atlas cells together at grazing angles (deep aniso LOD), and at the
|
||||
// page border the wrap pulled in the opposite edge of the page: the
|
||||
// reflective_trans canopy slope (cells at v ~ 0.996, white-cliff
|
||||
// cells wrapping in from v ~ 0) glowed ~2x bright while the right
|
||||
// awning went dark, with the decode, UVs and constants all verified
|
||||
// exact (diagnosed via the mode-7 raw-lightmap isolation view).
|
||||
float3 lmg = lightmap.SampleLevel(smp_clamp, i.uv2, 0.0).rgb;
|
||||
// F12 isolation mode 7: visualize the RAW lightmap sample; mode 8:
|
||||
// visualize the lightmap unwrap coordinate (frac(uv2*16) in rg).
|
||||
// Debug-only taps on fams 5/6/13 (misc.z is 0 on the other fams).
|
||||
if (abs(misc.z - 7.0) < 0.5) {
|
||||
return float4(lmg, 1.0);
|
||||
}
|
||||
if (abs(misc.z - 8.0) < 0.5) {
|
||||
return float4(frac(i.uv2 * 16.0), 0.0, 1.0);
|
||||
}
|
||||
// Mode 9: lightmap-resolve status; RED = a real lightmap is bound
|
||||
// (tint.r set by the C++ resolve), BLUE = white fallback in t1.
|
||||
if (abs(misc.z - 9.0) < 0.5) {
|
||||
return float4(tint.r, 0.0, 1.0 - tint.r, 1.0);
|
||||
}
|
||||
float3 lml = min(lmg * lmg, s + sh_color.rgb);
|
||||
// GetTangentLight with the neutral (flat) normal map:
|
||||
// 0.39 * 2.39562 exactly. Fam 13 has no kd term at all; its body is
|
||||
// D^2 * lml * ALPHA, premultiplied once in the shader on top of the
|
||||
// a^2 blend factor (verified 0.0-error vs the ucode).
|
||||
lin = fam > 12.5 ? lml * dcol * albedo.a : lml * 0.93429 * dcol;
|
||||
if (overlay.w > 2.5) {
|
||||
// spec/ecc/refmask at t4: phong vs the FIXED literal light
|
||||
// (-0.14, 0.5, 0.9), power 10 + 290*ecc, tint (2.1, 1.8, 1.5),
|
||||
// scaled by the clamped lightmap green and the spec mask.
|
||||
float4 masks = decal_art.Sample(smp, i.uv);
|
||||
float3 wn = dot(i.nrm, i.nrm) > 0.01
|
||||
? normalize(i.nrm)
|
||||
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
|
||||
// misc.z = 3 (F12 reflection isolation): force the flat normal.
|
||||
if (overlay.w > 3.5 && abs(misc.z - 3.0) > 0.5) {
|
||||
// Per-pixel normal map (t5, paired descriptor): the real PS
|
||||
// (baseenvironmentreflective_defaultPS) reflects off the
|
||||
// normal-mapped normal: tangent normal = 2*(n + detail - 1) on
|
||||
// xy, 2*n.z - 1 on z (the material's detail map is a constant
|
||||
// neutral 16x16, folded here as 0.5). With the FLAT vertex normal
|
||||
// every panel of a glass facade reflects the same tiny cube
|
||||
// region: one giant magnified smear of the plaza cube's
|
||||
// lamp-heads/trees (the "streetlight head" artifact, ucode-traced
|
||||
// on a captured pixel; flat N lands on the
|
||||
// face-0 tree/lamp texels, the mapped N tilts onto sky). The
|
||||
// per-panel tilts break the reflection up exactly like the
|
||||
// emulated frame. Screen-space cotangent frame (same shape as
|
||||
// the vehicle DXN branch; the world VS carries no tangent
|
||||
// attributes we decode).
|
||||
// F12 mode 6: the slider drives the normal-map LOD bias live;
|
||||
// the console fetches the nm at its 640p gradient LOD (blurrier,
|
||||
// weaker bump tilts), so the matching bias is the remaining
|
||||
// reflection-rotation candidate. Default stays SHARP (bias 0):
|
||||
// an unconditional misc.y bias visibly degraded the glass;
|
||||
// tune with the
|
||||
// slider first, promote the found value to a default after.
|
||||
float nm_bias = misc.z > 5.5 ? misc.w : 0.0;
|
||||
float3 nmv = normal_map.SampleBias(smp, i.uv, nm_bias).rgb;
|
||||
// Exact composition: xy = 2*normal + 2*detail - 2, z = 2*n.z - 1.
|
||||
// The detail map is a CONSTANT 16x16 (0.514, 0.506), NOT the
|
||||
// formula's 0.5 neutral, so its fold is a constant tangent tilt.
|
||||
// The tilt rides the two F12 trim sliders (packed in misc.x;
|
||||
// defaults = the exact fold) so the residual reflection rotation
|
||||
// can be dialed live against the emulated frame.
|
||||
float trim_yi = floor(misc.x / 1000.0);
|
||||
float2 trim = float2(misc.x - trim_yi * 1000.0 - 500.0, trim_yi - 500.0) *
|
||||
0.001;
|
||||
float3 nt = float3(nmv.xy * 2.0 - 1.0 + trim, nmv.z * 2.0 - 1.0);
|
||||
float3 dp1 = ddx(i.rpos), dp2 = ddy(i.rpos);
|
||||
float2 du1 = ddx(i.uv), du2 = ddy(i.uv);
|
||||
float3 dp2p = cross(dp2, wn), dp1p = cross(wn, dp1);
|
||||
float3 tt = dp2p * du1.x + dp1p * du2.x;
|
||||
float3 bb = dp2p * du1.y + dp1p * du2.y;
|
||||
// Signs/lengths calibrated against the mesh's REAL stored TBN
|
||||
// (10_11_11 tangent @ +24, s16 normal xy @ +20, handedness in the
|
||||
// unwrap sign bits, decoded mesh-wide):
|
||||
// stored T == +dP/dU on 100% of tris (this formula's tt comes out
|
||||
// as -dP/dU -> negate), stored B == this bb direction (dot 0.999),
|
||||
// and the game's rows are UNIT so each axis normalizes SEPARATELY
|
||||
// - the shared max-normalizer both halved and (with the flip)
|
||||
// mirrored the horizontal tilt, which pushed the reflection
|
||||
// DEEPER into the dark cube region instead of off it (first
|
||||
// deploy looked unchanged). With these signs the reconstruction
|
||||
// matches the ucode's mapped normal to 4 decimals at the traced
|
||||
// pixel. An inward geometric wn flips all three terms together;
|
||||
// reflect() is invariant to that.
|
||||
tt *= -rsqrt(max(dot(tt, tt), 1e-12));
|
||||
bb *= rsqrt(max(dot(bb, bb), 1e-12));
|
||||
// The mesh's STORED TBN (what the game's VS interpolates) is the
|
||||
// world-up frame, not the exact UV-gradient frame: B = up
|
||||
// projected into the surface, T = cross(B, N), verified on the
|
||||
// decoded window vertices (B = (0, 0.999, 0), T = horizontal).
|
||||
// The screen-space frame lands ~3 deg off it (authored TBN vs UV
|
||||
// gradients), which rotates reflections by degrees, visible as
|
||||
// parallax against the emulated frame. Use the analytic axes and
|
||||
// keep only the screen-space frame's SIGNS (mirrored UV islands
|
||||
// flip the tangent; derivatives track that, the analytic frame
|
||||
// cannot). Near-horizontal surfaces keep the screen frame.
|
||||
float3 bb2 = float3(0.0, 1.0, 0.0) - wn * wn.y;
|
||||
float lb2 = length(bb2);
|
||||
if (lb2 > 0.05) {
|
||||
bb2 /= lb2;
|
||||
float3 tt2 = cross(bb2, wn);
|
||||
tt = tt2 * (dot(tt2, tt) >= 0.0 ? 1.0 : -1.0);
|
||||
bb = bb2 * (dot(bb2, bb) >= 0.0 ? 1.0 : -1.0);
|
||||
}
|
||||
wn = normalize(nt.x * tt + nt.y * bb + wn * max(nt.z, 0.05));
|
||||
}
|
||||
float3 vd = -normalize(i.rpos);
|
||||
float3 Ls = float3(-0.14, 0.5, 0.9);
|
||||
float3 refl = Ls - 2.0 * wn * dot(wn, Ls);
|
||||
float bp = saturate(dot(vd, -refl));
|
||||
float ks = pow(max(bp, 1e-6), 10.0 + 290.0 * masks.y);
|
||||
// misc.z = 5 (F12 reflection isolation): body only, no spec/cube.
|
||||
if (abs(misc.z - 5.0) > 0.5) {
|
||||
lin += ks * float3(2.1, 1.8, 1.5) * lml.g * masks.x;
|
||||
}
|
||||
if (((fam > 4.5 && fam < 6.5) || fam > 12.5) &&
|
||||
abs(misc.z - 5.0) > 0.5 && abs(misc.z - 1.0) > 0.5) {
|
||||
// Cube reflection: reflect(E, wN) with xy negated (the source's
|
||||
// ref_vec.xy *= -1), luminosity lerped toward 1 by
|
||||
// 0.3 * sat(4*refmask - 2.6), x refmask x reflection_scale 1.5.
|
||||
float3 rv = vd - 2.0 * wn * dot(vd, wn);
|
||||
// misc.y = cube LOD bias log2(render_height / 640): the guest
|
||||
// computes the cube fetch's gradient LOD at its own 1152x640
|
||||
// render; at 4K our per-pixel gradients are ~3.4x smaller, so
|
||||
// without the bias baked cube detail (the plaza streetlight
|
||||
// heads) survives through mips the console's fetch blurs away.
|
||||
// F12 isolation (misc.z): mode 2 samples the ABSOLUTE level in
|
||||
// misc.w; other modes add misc.w as extra bias; mode 4 shows the
|
||||
// raw cube sample in place of the shaded result.
|
||||
float3 dir = float3(-rv.x, -rv.y, rv.z);
|
||||
// Mode 6 gives the slider to the NM fetch; the cube keeps just
|
||||
// the automatic bias there.
|
||||
float cube_extra = misc.z > 5.5 ? 0.0 : misc.w;
|
||||
float3 cube = abs(misc.z - 2.0) < 0.5
|
||||
? env_cube.SampleLevel(smp, dir, misc.w).rgb
|
||||
: env_cube.SampleBias(smp, dir, misc.y + cube_extra).rgb;
|
||||
float rl = 0.3 * saturate(4.0 * masks.z - 2.6);
|
||||
float lum = lml.g + rl * (1.0 - lml.g);
|
||||
lin += cube * lum * masks.z * 1.5;
|
||||
if (abs(misc.z - 4.0) < 0.5) {
|
||||
lin = cube * cube;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fam > 12.5) {
|
||||
// Fam 13 blend factor: the PS outputs a^2 (straight-alpha blend on
|
||||
// top of the in-shader premultiplied body; wisps thin as ~a^3,
|
||||
// same convention as transparentenvironment).
|
||||
out_a = albedo.a * albedo.a;
|
||||
} else if (fam > 6.5) {
|
||||
out_a = albedo.a;
|
||||
reduced_tone = fam > 7.5; // environmentdiffuse's cheap tonemap
|
||||
}
|
||||
fog_a *= sh_env.x; // material multiplier (PS c11.y)
|
||||
}
|
||||
// Fog -> exposure -> tonemap -> sqrt, then the postfx uber's measured
|
||||
// 1.41 scene multiplier (same as the character branch).
|
||||
float3 xe = (lin * fog_a + fog_rgb) * expo;
|
||||
float3 t1e = saturate(1.0 - xe);
|
||||
float3 tme = reduced_tone ? 1.0 - t1e * t1e
|
||||
: max(xe * 0.25 + 0.75, 1.0) - t1e * t1e;
|
||||
float3 cce = saturate(sqrt(max(tme * 0.5, 0.0)) * 1.41);
|
||||
return float4(cce, out_a);
|
||||
}
|
||||
// Macro overlay: large-scale grime/cracks multiplied over the diffuse at
|
||||
// uv * macroOverlayUVScale, faded by macroOverlayOpacity: the ground and
|
||||
// wall weathering. WHITE is the neutral (materials without weathering
|
||||
// bind a 16x16 "default_white"). The game multiplies it ONCE in its
|
||||
// linear (squared) color space, so the gamma-space equivalent is
|
||||
// sqrt(m); a direct multiply doubles the darkening (harsh black
|
||||
// patchwork vs the emulated subtle weathering).
|
||||
if (overlay.z > 0.0 && misc.x < 1.5) { // water reuses overlay.z (ripple map flag)
|
||||
float4 m = macro.Sample(smp, i.uv * overlay.x);
|
||||
albedo.rgb *= lerp(float3(1.0, 1.0, 1.0), sqrt(m.rgb), overlay.y * m.a);
|
||||
}
|
||||
// environment.decal surfaces: the paint/graffiti art (t4) is composited
|
||||
// over the base diffuse by ITS alpha, opaque output; these meshes ARE
|
||||
// the wall/ground there. The art maps with uv3, the second half-pair of
|
||||
// the packed half4 first texcoord (validated offline: sampling with the
|
||||
// tiling uv0 repeats it: "Stereo Stereo Stereo"; the fmt-26 second
|
||||
// element is the lightmap unwrap, not the decal's).
|
||||
if (overlay.w > 0.0) {
|
||||
// overlay.w == 2 marks environment.decal_tileable: the art tiles across
|
||||
// the surface (rock/cliff faces) and must WRAP; clamp stretched the
|
||||
// border texels into giant streaks. Single placements clamp (their
|
||||
// transparent border keeps everything outside the placement clear).
|
||||
float4 dk = overlay.w > 1.5 ? decal_art.Sample(smp, i.uv3)
|
||||
: decal_art.Sample(smp_clamp, i.uv3);
|
||||
albedo.rgb = lerp(albedo.rgb, dk.rgb, dk.a);
|
||||
}
|
||||
// tint.r > 0 marks items with a lightmap bound (2x baked lighting);
|
||||
// otherwise fall back to derivative face shading. The lighting term stays
|
||||
// separate from the albedo so the CSM receive below can min-clamp IT, the
|
||||
// way the game's GetShadowedLightMap clamps the lightmap lighting.
|
||||
float3 light;
|
||||
if (tint.b > 0.0) {
|
||||
light = float3(1.0, 1.0, 1.0); // unlit (sky dome)
|
||||
} else if (tint.r > 0.0) {
|
||||
light = lightmap.Sample(smp, i.uv2).rgb * 2.0;
|
||||
} else {
|
||||
// Smooth per-vertex normal when the mesh has one; face normal from
|
||||
// position derivatives otherwise.
|
||||
float3 n = dot(i.nrm, i.nrm) > 0.01 ? normalize(i.nrm)
|
||||
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
|
||||
light = abs(dot(n, normalize(float3(0.4, 0.8, 0.3)))) * 0.35 + 0.75;
|
||||
}
|
||||
// Dynamic CSM shadow receive (world geometry + rigid props; characters
|
||||
// need the game's separate PCF/bias variant; skipping them avoids
|
||||
// self-shadow acne, and the ground shadow is 95% of the visible effect).
|
||||
// Exact receiver math from the baseenvironment PS disassembly: finest
|
||||
// cascade whose |ls| < 0.99 wins; shadow = saturate(infront + 1 -
|
||||
// coverage), then the game min-clamps the LINEAR lighting term:
|
||||
// light_linear = min(light_linear, s + c8.rgb)
|
||||
// Full shadow clamps to the dim bluish c8 ambient, the penumbra saturates
|
||||
// wherever the clamp exceeds the lit level (which is what keeps the edge
|
||||
// crisp), and surfaces already darker than the clamp, baked shade under
|
||||
// bridges/trees, show NO dynamic shadow at all. Our light term is
|
||||
// gamma-space (light^2 ~ the game's linear term: the lightmap x2 folds
|
||||
// its x4 linear multiplier), so the clamp maps to min(light, sqrt(s+c8))
|
||||
// per channel. A fixed-denominator curve here read pitch-black and
|
||||
// double-darkened baked shade.
|
||||
if (sh_misc.y > 0.0 && tint.g == 0.0 && tint.b == 0.0 && misc.x == 0.0) {
|
||||
float3 wp = i.rpos + cam_pos.xyz;
|
||||
float2 lsv = float2(dot(sh_x.xyz, wp) + sh_x.w, dot(sh_y.xyz, wp) + sh_y.w);
|
||||
float rd = dot(sh_z.xyz, wp) + sh_z.w - sh_misc.x;
|
||||
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) {
|
||||
float2 uv = float2(luv.x / 6.0 + (casc * 2.0 - 1.0) / 6.0, luv.y * -0.5 + 0.5);
|
||||
float2 m = shadow_atlas.Sample(smp_clamp, uv);
|
||||
float s = saturate((m.x >= rd ? 1.0 : 0.0) + (1.0 - m.y));
|
||||
light = min(light, sqrt(s + sh_color.rgb));
|
||||
}
|
||||
}
|
||||
float3 lit = albedo.rgb * light;
|
||||
if (mat_tint.w > 0.0 && misc.x == 0.0) {
|
||||
lit *= mat_tint.rgb;
|
||||
}
|
||||
if (misc.x > 1.5) {
|
||||
// water.* (flowingwater.fx approximation): the real shader is
|
||||
// near-black diffuse + dual time-scrolled ripple-normal taps + an
|
||||
// environment-cube reflection + sun specular. We have no cube map
|
||||
// bound, so the reflection term is the frame fog color (the best
|
||||
// single approximation of the surroundings' haze tone) scaled by a
|
||||
// fresnel curve; the ripple normal perturbs both the fresnel and a sun
|
||||
// sparkle along the captured CSM light axis. The lightmap (x2) keeps
|
||||
// the baked bridge/wall shading on the surface. Calibrated against the
|
||||
// canal capture (emulated mid-canal mean ~(24,28,32)/255).
|
||||
float t = overlay.x;
|
||||
float2 rip;
|
||||
if (overlay.z > 0.0) {
|
||||
// Dual scrolled taps of the material's ripple normal map (macro slot).
|
||||
float2 wuv = i.uv * 6.0;
|
||||
float3 n1 = macro.Sample(smp, wuv + t * float2(0.11, 0.06)).rgb;
|
||||
float3 n2 = macro.Sample(smp, wuv * 1.71 - t * float2(0.07, 0.13)).rgb;
|
||||
rip = (n1.xy + n2.xy) - 1.0;
|
||||
} else {
|
||||
// Normal map unresolved: procedural ripples from world position.
|
||||
// Wavelengths ~0.4-1m (emulated ripples are decimeter-scale); low
|
||||
// frequencies formed meter-wide chevron interference bands that read
|
||||
// as giant arrows on the surface.
|
||||
float3 wp = i.rpos + cam_pos.xyz;
|
||||
rip = 0.35 * float2(sin(wp.x * 9.7 + wp.z * 6.1 + t * 2.3) +
|
||||
0.6 * sin(wp.x * 17.3 - wp.z * 11.9 + t * 3.4),
|
||||
cos(wp.x * 7.1 - wp.z * 13.7 + t * 2.7) +
|
||||
0.6 * cos(wp.x * 12.9 + wp.z * 18.3 + t * 3.1));
|
||||
}
|
||||
float3 wn = normalize(float3(rip.x * 0.4, 2.0, rip.y * 0.4));
|
||||
float3 vd = -normalize(i.rpos);
|
||||
float fres = pow(1.0 - saturate(dot(vd, wn)), 3.0);
|
||||
// The flowing-water lightmap unwrap decodes unreliably (bands across
|
||||
// atlas gutters), so the water term deliberately ignores it: near-black
|
||||
// body + ripple-perturbed cube reflection + sun sparkle.
|
||||
// Deep body: the water "diffuse" is a faint STRIPE MASK (max 24/255,
|
||||
// WaterFallFoamAlpha, a lookup for the real shader, not a color). The
|
||||
// game consumes it in linear space where 0.09^2 vanishes; squaring here
|
||||
// likewise kills the visible blue/black banding. overlay.w > 0 = no
|
||||
// diffuse channel at all (ocean.default); body is zero there, NOT the
|
||||
// white fallback (ocean.fx: diffTerm = (0,0,0), color is all reflection).
|
||||
float3 col = overlay.w > 0.5 ? 0.0 : albedo.rgb * albedo.rgb * 0.6;
|
||||
// Reflection tint: the environment cube when resolved (t6); otherwise a
|
||||
// haze derived from the frame fog color, lifted toward neutral so dark
|
||||
// dusk fog doesn't collapse the water to black (fit: emulated canal
|
||||
// mean ~(24,28,32)/255 with fog color (0.02,0.07,0.13)).
|
||||
float3 renv = overlay.y > 0.0
|
||||
? env_cube.Sample(smp, reflect(-vd, wn)).rgb
|
||||
: mat_tint.rgb * 0.5 + 0.06;
|
||||
col += renv * (0.55 + 0.45 * fres);
|
||||
if (sh_misc.y > 0.0) {
|
||||
float3 h = normalize(vd + normalize(-sh_z.xyz));
|
||||
col += pow(saturate(dot(wn, h)), 90.0) * 0.35; // sun sparkle
|
||||
}
|
||||
float fade = saturate(length(i.rpos) * misc.y + misc.z);
|
||||
if (misc.w != 1.0) {
|
||||
fade = pow(max(fade, 1e-4), misc.w);
|
||||
}
|
||||
col = sqrt(max(col * col * saturate(1.0 + fade * mat_tint.w) +
|
||||
fade * mat_tint.rgb, 0.0));
|
||||
// Opaque: the game's murk hides the canal bed entirely (and our bed
|
||||
// shading is untrustworthy under water: striped lightmap unwraps).
|
||||
return float4(col, 1.0);
|
||||
}
|
||||
if (misc.x > 0.0) {
|
||||
// transparentenvironment.fx (Skate 2 source; disassembled Skate 3 PS
|
||||
// matches): outcolor.rgb = diffTerm * diffuse.rgb * diffuse.a; the rgb
|
||||
// is premultiplied by alpha ONCE IN THE SHADER on top of the a^2 blend
|
||||
// factor, so wisps thin out as ~a^3. That cubic falloff is most of the
|
||||
// emulated "sparse clouds" look. Fog is applied in the game's linear
|
||||
// (squared) color space: fade = saturate(dist * ramp.x + ramp.y)^ramp.z
|
||||
// toward the fog color, transmittance = 1 + fade * fogcolor.w.
|
||||
lit *= albedo.a;
|
||||
float fade = saturate(length(i.rpos) * misc.y + misc.z);
|
||||
if (misc.w != 1.0) {
|
||||
fade = pow(max(fade, 1e-4), misc.w);
|
||||
}
|
||||
lit = sqrt(max(lit * lit * saturate(1.0 + fade * mat_tint.w) +
|
||||
fade * mat_tint.rgb, 0.0));
|
||||
return float4(lit, albedo.a * albedo.a);
|
||||
}
|
||||
return float4(lit, 1.0);
|
||||
}
|
||||
// Shadow caster pass: vs_main runs with mvp = (world *) lightVP built from
|
||||
// the captured receiver rows, so SV_Position.z IS the light-space depth
|
||||
// (the height-ramp row; viewport z range 0..1, DepthClip off so casters
|
||||
// outside the 12 m depth window clamp like the game accepts). MIN blend on
|
||||
// both channels against a (1, 1) clear: R accumulates the min depth, G
|
||||
// drops to 0 where any caster drew ("uncoverage"; the blur pass converts
|
||||
// to the game's coverage convention).
|
||||
float2 ps_shadow_caster(VSOut i) : SV_Target {
|
||||
return float2(i.pos.z, 0.0);
|
||||
}
|
||||
45
src/native/shaders/shadow_blur.hlsl
Normal file
45
src/native/shaders/shadow_blur.hlsl
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// Shadow blur: the game's shadow_h/vblur passes, exact semantics from the
|
||||
// disassembled Xenos ucode. One fullscreen-triangle draw
|
||||
// per tile per direction; taps stay inside the tile (clamped). Coverage is
|
||||
// Gaussian-blurred; depth keeps the exact center value where covered and
|
||||
// dilates (binary-coverage-weighted neighbour average) into the penumbra
|
||||
// otherwise, so depth compares stay valid across the blurred edge.
|
||||
// b0 floats: c0 = (dir.x, dir.y, ntaps, src_is_raw), c1 = (w0, w1, w2, 0),
|
||||
// c2 = (tile_x0, tile_x1, tile_y1, 0). ntaps 0 = format-convert only
|
||||
// (cascade 2 is never blurred).
|
||||
cbuffer C : register(b0) {
|
||||
float4 c0;
|
||||
float4 c1;
|
||||
float4 c2;
|
||||
};
|
||||
Texture2D<float2> src : register(t0);
|
||||
float4 vs_main(uint id : SV_VertexID) : SV_Position {
|
||||
float2 uv = float2((id << 1) & 2, id & 2);
|
||||
return float4(uv * float2(2, -2) + float2(-1, 1), 0, 1);
|
||||
}
|
||||
float2 ps_main(float4 pos : SV_Position) : SV_Target {
|
||||
int2 p = int2(pos.xy);
|
||||
int ntaps = int(c0.z);
|
||||
float w[3] = {c1.x, c1.y, c1.z};
|
||||
float2 center = src.Load(int3(p, 0));
|
||||
float ccen = c0.w > 0.0 ? 1.0 - center.y : center.y;
|
||||
float cov = 0.0;
|
||||
float dsum = 0.0;
|
||||
float dcnt = 0.0;
|
||||
[unroll] for (int k = -2; k <= 2; ++k) {
|
||||
if (abs(k) > ntaps) continue;
|
||||
int2 q = p + int2(c0.xy) * k;
|
||||
q.x = clamp(q.x, int(c2.x), int(c2.y));
|
||||
q.y = clamp(q.y, 0, int(c2.z));
|
||||
float2 t = src.Load(int3(q, 0));
|
||||
float c = c0.w > 0.0 ? 1.0 - t.y : t.y;
|
||||
cov += w[abs(k)] * c;
|
||||
if (k != 0) {
|
||||
float hard = c > 0.001 ? 1.0 : 0.0;
|
||||
dcnt += hard;
|
||||
dsum += hard * t.x;
|
||||
}
|
||||
}
|
||||
float depth = ccen > 0.001 ? center.x : (dcnt > 0.0 ? dsum / dcnt : center.x);
|
||||
return float2(depth, saturate(cov));
|
||||
}
|
||||
47
src/native/shaders/spline.hlsl
Normal file
47
src/native/shaders/spline.hlsl
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// In-world neon spline shader (waypoint arrows / marker beams). The guest
|
||||
// B-spline VS is evaluated on the CPU at publish time into WORLD-space
|
||||
// vertices; the VS here projects them with the scene's (smoothed) view_proj
|
||||
// rows so the signs stay glued to the world under camera re-timing. The two
|
||||
// PS variants transcribe the game's own spline.fx (Skate-Shaders repo):
|
||||
// "default" = additive glow with the squared-gamma trick, "darken" =
|
||||
// straight-alpha backdrop dimming. The gradient texture uses the wrap/aniso
|
||||
// sampler like the original i_diffuse (U runs 0..N along the band).
|
||||
cbuffer C : register(b0) {
|
||||
float4 vp0; // scene view_proj rows (row-vector: clip = x*vp0+y*vp1+z*vp2+w*vp3)
|
||||
float4 vp1;
|
||||
float4 vp2;
|
||||
float4 vp3;
|
||||
float4 intensity; // i_intensity as staged (x = default gain, y = darken gain)
|
||||
};
|
||||
Texture2D<float4> tex : register(t0);
|
||||
SamplerState smp : register(s0);
|
||||
struct VSOut {
|
||||
float4 pos : SV_Position;
|
||||
float2 uv : TEXCOORD0;
|
||||
float fade : TEXCOORD1;
|
||||
};
|
||||
VSOut vs_main(float4 p : POSITION, float2 uv : TEXCOORD0, float fade : TEXCOORD1) {
|
||||
VSOut o;
|
||||
o.pos = p.x * vp0 + p.y * vp1 + p.z * vp2 + p.w * vp3;
|
||||
o.uv = uv;
|
||||
o.fade = fade;
|
||||
return o;
|
||||
}
|
||||
// Exact transcription of the Skate 3 spline pixel shaders (disassembled
|
||||
// ucode, NOT the older Skate 2 spline.fx source: EA added in-shader sqrt
|
||||
// gamma compensation). default: oC0.rgb = sqrt(rgb^2 * i.x * fade / a);
|
||||
// darken: oC0.rgb = sqrt(rgb^2 * i.y), oC0.a = sqrt(a * i.y * fade).
|
||||
// Getting this wrong is visible: a linear 1/a over-brightens the low-alpha
|
||||
// glow fringe (fuzzy, blown-out edges) and a linear darken alpha weakens the
|
||||
// backdrop dimming that makes the neon read as solid.
|
||||
float4 ps_default(VSOut i) : SV_Target {
|
||||
float4 c = tex.Sample(smp, i.uv);
|
||||
float3 sq = c.rgb * c.rgb * (intensity.x * i.fade / max(c.a, 1.0 / 255.0));
|
||||
return float4(sqrt(abs(sq)), 1.0);
|
||||
}
|
||||
float4 ps_darken(VSOut i) : SV_Target {
|
||||
float4 c = tex.Sample(smp, i.uv);
|
||||
float3 rgb = sqrt(abs(c.rgb * c.rgb * intensity.y));
|
||||
float a = sqrt(abs(c.a * intensity.y * i.fade));
|
||||
return float4(rgb, a);
|
||||
}
|
||||
174
src/native/skate3_native_diag.h
Normal file
174
src/native/skate3_native_diag.h
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
#pragma once
|
||||
|
||||
// Internal diagnostics interface shared between skate3_native_scene.cpp and
|
||||
// skate3_native_diagnostics.cpp: the offline-analysis recording
|
||||
// (StartRecording/WriteRecording), the camera/bone signal recorders and the
|
||||
// synthetic-pan probe. Pure diagnosis machinery; nothing here participates
|
||||
// in rendering; capture call sites live in the scene TU because they read
|
||||
// per-draw locals, but all state and file writing lives in the diagnostics
|
||||
// TU.
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "skate3_native_scene.h"
|
||||
|
||||
namespace skate3::native_scene {
|
||||
|
||||
// ---- Offline-analysis recording (see StartRecording/WriteRecording) --------
|
||||
// The full per-draw constant bank stream is the ground truth for what the
|
||||
// game's shaders saw; the per-frame item lists are what our pipeline made of
|
||||
// it.
|
||||
struct RecordedDraw {
|
||||
uint32_t frame;
|
||||
uint32_t func; // 0 = DrawIndexedVertices, 1 = DrawVertices, 2 = BeginVertices
|
||||
uint32_t ib;
|
||||
uint32_t vb;
|
||||
uint32_t vb_offset; // stream-0 OffsetInBytes at bind time
|
||||
uint32_t vb_stride; // stream-0 stride at bind time
|
||||
uint32_t streams[4][3]; // all streams: {vb, offset, stride}
|
||||
uint32_t vfetch[12]; // fetch-constant shadow groups 0-1 (device+0x480)
|
||||
uint32_t args[4];
|
||||
float bank[1024]; // VS c0..c255
|
||||
float ps[256]; // PS c0..c63 (material colors, e.g. CAS hair tint)
|
||||
// 2D recon fields (SK3DRAW7):
|
||||
uint32_t flags2d; // bit per active 2D bracket (see On2dPhase)
|
||||
uint32_t ps_obj; // current guest pixel/vertex shader objects
|
||||
uint32_t vs_obj;
|
||||
uint32_t viewport[6]; // last SetViewport payload (raw guest dwords)
|
||||
uint32_t scissor[4]; // last SetScissorRect payload
|
||||
uint32_t rstates[256]; // render-state shadow snapshot (blend/depth)
|
||||
uint32_t vfetch_all[192]; // full 32-slot texture fetch shadow
|
||||
uint32_t vb_dump; // index into buffers.bin for this draw's payloads
|
||||
uint32_t ib_dump; // (~0u = none)
|
||||
};
|
||||
struct RecordedFrame {
|
||||
uint64_t generation;
|
||||
float view_proj[16];
|
||||
float cam_pos[3];
|
||||
std::vector<DrawItem> dynitems;
|
||||
std::vector<DrawItem> items;
|
||||
};
|
||||
// Dynamic meshes live in streaming arenas that are recycled DURING a long
|
||||
// recording; the end-of-window .gsnap holds garbage for them. Dump each
|
||||
// captured buffer payload once per content fingerprint so offline tools
|
||||
// decode exactly what the game used.
|
||||
struct RecordedBuffer {
|
||||
uint32_t vb_addr;
|
||||
uint32_t ib_addr;
|
||||
uint64_t fingerprint;
|
||||
std::vector<uint8_t> vb;
|
||||
std::vector<uint8_t> ib;
|
||||
};
|
||||
// BeginVertices (inline ring) payloads are written by the CPU AFTER the
|
||||
// call returns; dump them at frame end, when the writes are complete but
|
||||
// the ring has not yet wrapped. draw_index links the dump back to its
|
||||
// RecordedDraw.
|
||||
struct PendingInlineDump {
|
||||
uint32_t addr;
|
||||
uint32_t bytes;
|
||||
size_t draw_index;
|
||||
};
|
||||
|
||||
extern std::mutex g_record_mutex;
|
||||
extern std::atomic<bool> g_recording;
|
||||
extern uint32_t g_record_frame; // index of the NEXT recorded frame
|
||||
extern uint32_t g_record_stride; // record every Nth frame
|
||||
extern uint32_t g_frames_seen; // frames completed since StartRecording
|
||||
extern std::vector<std::unique_ptr<RecordedDraw>> g_recorded_draws;
|
||||
extern std::vector<RecordedFrame> g_recorded_frames;
|
||||
extern std::vector<RecordedBuffer> g_recorded_buffers;
|
||||
extern std::unordered_set<uint64_t> g_recorded_buffer_keys;
|
||||
extern size_t g_recorded_buffer_bytes;
|
||||
extern std::vector<PendingInlineDump> g_pending_inline_dumps;
|
||||
// One payload dump per (guest address, frame): 2D dynamic buffers hold many
|
||||
// draws' vertices; the repeated full-buffer dump would be pure duplication.
|
||||
extern std::unordered_map<uint64_t, uint32_t> g_frame_dump_ids;
|
||||
|
||||
// ---- Camera-signal recorder (judder diagnosis) ------------------------------
|
||||
// Records the guest camera SIGNAL during a real stick pan: every distinct
|
||||
// pose the ~1 kHz sampler accepts (kind 0, precise host timestamps), the raw
|
||||
// per-frame pose at scene build (kind 2) and the smoother's output (kind 1,
|
||||
// with its playback time). Written as logs/cam_signal_<ts>.csv when the
|
||||
// window closes. Entry storage is private to the diagnostics TU; call sites
|
||||
// gate on the deadline before computing anything.
|
||||
extern std::atomic<double> g_camsig_deadline; // record until this host time
|
||||
|
||||
// Heading from a row-vector view matrix: camera forward in world space is
|
||||
// the view rotation's third column (v[2], v[6], v[10]).
|
||||
float YawFromViewRows(const float view[16]);
|
||||
|
||||
// Append one entry while the window is open (bounded; thread-safe).
|
||||
void CamSigPush(double t, double play_t, float yaw, uint8_t kind);
|
||||
|
||||
// Per-frame tick (guest render thread, scene build): appends the raw frame
|
||||
// pose (kind 2) and, when smoothed_rot is non-null, the smoother's output
|
||||
// (kind 1 at play_t); writes + resets once the window closes. Call only when
|
||||
// g_camsig_deadline > 0.
|
||||
void CamSigFrameTick(double rec_now, const float cam_view[16],
|
||||
const float* smoothed_rot, double play_t);
|
||||
|
||||
// ---- Bone-signal recorder (wheel-guard diagnosis) ---------------------------
|
||||
// Records the raw per-entity pose stream (every ring push: bone palettes /
|
||||
// rigid worlds with timestamps, kind 0) plus what the interpolation actually
|
||||
// output each frame (kind 1, with the playback time) to
|
||||
// logs/bone_signal_<ts>.bin. Binary records after a "BSIG1\n" header:
|
||||
// u8 kind, u64 key, f64 t, f64 play, u32 n, float[n]. Guest render thread
|
||||
// only (written off-thread at close).
|
||||
//
|
||||
// Per-frame arm/flush tick (guest render thread): consumes a pending
|
||||
// RecordBoneSignal request, returns true while the window is open, and
|
||||
// spawns the file write once it closes.
|
||||
bool BoneSigTick(double now);
|
||||
void BoneSigAppend(uint8_t kind, uint64_t key, double t, double play, const float* v,
|
||||
uint32_t n);
|
||||
|
||||
// ---- Synthetic camera pan (judder isolation probe) --------------------------
|
||||
// See the skate3_native_render_scene_synthetic_pan cvar comment for the mode
|
||||
// semantics. Engage state is captured from the RAW guest camera on the guest
|
||||
// render thread; the sampler thread reads it for mode 3 under g_synpan_mutex.
|
||||
extern std::atomic<int> g_synpan_active; // engaged mode (0 = off)
|
||||
extern std::mutex g_synpan_mutex;
|
||||
extern float g_synpan_view0[16]; // raw guest view at engage
|
||||
extern float g_synpan_proj0[16];
|
||||
extern float g_synpan_c0[3]; // camera world position at engage (held fixed)
|
||||
extern double g_synpan_t0;
|
||||
extern double g_synpan_step_phase; // mode 2: accumulated phase (degrees)
|
||||
extern double g_synpan_ema_dt; // mode 2: slow EMA of the publish dt
|
||||
// Telemetry (guest render thread only).
|
||||
extern uint64_t g_synpan_frames;
|
||||
extern double g_synpan_last_build;
|
||||
extern double g_synpan_dt_sum, g_synpan_dt_sum2;
|
||||
extern double g_synpan_dt_min, g_synpan_dt_max;
|
||||
extern double g_synpan_err_sum2, g_synpan_err_max;
|
||||
extern uint64_t g_synpan_err_n;
|
||||
|
||||
// Full-surround world union for the probe: the game only submits what ITS
|
||||
// frustum sees each frame, so a synthetic pan away from the real heading
|
||||
// would show an empty world (sky dome only). While the probe is engaged,
|
||||
// every static item published is accumulated by identity and appended to
|
||||
// each frame's scene; sweep the REAL camera around once with the stick
|
||||
// after engaging and the union fills in the whole surround, making a full
|
||||
// 360-degree spin (amplitude 0) usable. Guest render thread only.
|
||||
extern std::unordered_map<uint64_t, DrawItem> g_synpan_union;
|
||||
|
||||
uint64_t SynPanItemKey(const DrawItem& it);
|
||||
|
||||
// Pan phase (degrees of accumulated rotation) -> heading angle in degrees.
|
||||
// Triangle wave in [-amp, +amp] starting at 0 heading upward; amp <= 0 =
|
||||
// unbounded continuous rotation.
|
||||
double SynPanAngleDeg(double phase_deg, double amp);
|
||||
|
||||
// View matrix for the engage pose yawed by `angle_deg` about world up (+Y),
|
||||
// camera position held at the engage position. Row-vector convention
|
||||
// throughout (p_view = p * V): the 3x3 becomes Ry * R0 (world rotates first,
|
||||
// then the engage view, a level pan; direction sign is irrelevant for the
|
||||
// probe), translation row rebuilt as -c0 * R'.
|
||||
void SynPanView(double angle_deg, float view_out[16]);
|
||||
|
||||
} // namespace skate3::native_scene
|
||||
388
src/native/skate3_native_diagnostics.cpp
Normal file
388
src/native/skate3_native_diagnostics.cpp
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
// Native renderer diagnostics: offline-analysis recording, camera/bone
|
||||
// signal recorders, synthetic-pan probe. Extracted verbatim from
|
||||
// skate3_native_scene.cpp; no rendering logic lives here.
|
||||
|
||||
#include "native/skate3_native_diag.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
|
||||
#include <rex/logging.h>
|
||||
|
||||
namespace skate3::native_scene {
|
||||
|
||||
// ---- Offline-analysis recording ---------------------------------------------
|
||||
|
||||
std::mutex g_record_mutex;
|
||||
std::atomic<bool> g_recording{false};
|
||||
uint32_t g_record_frame = 0; // index of the NEXT recorded frame
|
||||
uint32_t g_record_stride = 1; // record every Nth frame
|
||||
uint32_t g_frames_seen = 0; // frames completed since StartRecording
|
||||
std::vector<std::unique_ptr<RecordedDraw>> g_recorded_draws;
|
||||
std::vector<RecordedFrame> g_recorded_frames;
|
||||
std::vector<RecordedBuffer> g_recorded_buffers;
|
||||
std::unordered_set<uint64_t> g_recorded_buffer_keys;
|
||||
size_t g_recorded_buffer_bytes = 0;
|
||||
std::vector<PendingInlineDump> g_pending_inline_dumps;
|
||||
std::unordered_map<uint64_t, uint32_t> g_frame_dump_ids;
|
||||
|
||||
void StartRecording(uint32_t stride) {
|
||||
std::lock_guard<std::mutex> lock(g_record_mutex);
|
||||
g_recorded_draws.clear();
|
||||
g_recorded_frames.clear();
|
||||
g_recorded_buffers.clear();
|
||||
g_recorded_buffer_keys.clear();
|
||||
g_recorded_buffer_bytes = 0;
|
||||
g_pending_inline_dumps.clear();
|
||||
g_frame_dump_ids.clear();
|
||||
g_record_frame = 0;
|
||||
g_frames_seen = 0;
|
||||
g_record_stride = stride == 0 ? 1 : stride;
|
||||
g_recording.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void WriteRecording(const char* dir, const char* stem) {
|
||||
g_recording.store(false, std::memory_order_relaxed);
|
||||
std::lock_guard<std::mutex> lock(g_record_mutex);
|
||||
const std::filesystem::path base_path = std::filesystem::path(dir) / stem;
|
||||
|
||||
// Binary draw stream: header "SK3DRAW7", fixed records {u32 frame, u32
|
||||
// func, u32 ib, u32 vb, u32 vb_offset, u32 vb_stride, u32 streams[4][3],
|
||||
// u32 vfetch[12], u32 args[4], f32 vs_bank[1024], f32 ps_bank[256],
|
||||
// u32 flags2d, u32 ps_obj, u32 vs_obj, u32 viewport[6], u32 scissor[4],
|
||||
// u32 rstates[256], u32 vfetch_all[192], u32 vb_dump, u32 ib_dump}
|
||||
// (little-endian). func: 0 DrawIndexedVertices, 1 DrawVertices,
|
||||
// 2 BeginVertices. flags2d: bit0 FrontEndManager::Render2D, bit1
|
||||
// AptMovieIntegration::Render, bit2 DrawRenderingUnit. vb_dump/ib_dump
|
||||
// index into buffers.bin records (~0u = none).
|
||||
{
|
||||
std::ofstream out(base_path.string() + ".draws.bin", std::ios::binary);
|
||||
out.write("SK3DRAW7", 8);
|
||||
for (const auto& d : g_recorded_draws) {
|
||||
out.write(reinterpret_cast<const char*>(&d->frame), 4);
|
||||
out.write(reinterpret_cast<const char*>(&d->func), 4);
|
||||
out.write(reinterpret_cast<const char*>(&d->ib), 4);
|
||||
out.write(reinterpret_cast<const char*>(&d->vb), 4);
|
||||
out.write(reinterpret_cast<const char*>(&d->vb_offset), 4);
|
||||
out.write(reinterpret_cast<const char*>(&d->vb_stride), 4);
|
||||
out.write(reinterpret_cast<const char*>(d->streams), 48);
|
||||
out.write(reinterpret_cast<const char*>(d->vfetch), 48);
|
||||
out.write(reinterpret_cast<const char*>(d->args), 16);
|
||||
out.write(reinterpret_cast<const char*>(d->bank), 4096);
|
||||
out.write(reinterpret_cast<const char*>(d->ps), 1024);
|
||||
out.write(reinterpret_cast<const char*>(&d->flags2d), 4);
|
||||
out.write(reinterpret_cast<const char*>(&d->ps_obj), 4);
|
||||
out.write(reinterpret_cast<const char*>(&d->vs_obj), 4);
|
||||
out.write(reinterpret_cast<const char*>(d->viewport), 24);
|
||||
out.write(reinterpret_cast<const char*>(d->scissor), 16);
|
||||
out.write(reinterpret_cast<const char*>(d->rstates), 1024);
|
||||
out.write(reinterpret_cast<const char*>(d->vfetch_all), 768);
|
||||
out.write(reinterpret_cast<const char*>(&d->vb_dump), 4);
|
||||
out.write(reinterpret_cast<const char*>(&d->ib_dump), 4);
|
||||
}
|
||||
}
|
||||
|
||||
// Captured buffer payloads: header "SK3BUFS1", then records {u32 vb_addr,
|
||||
// u32 ib_addr, u64 fingerprint, u32 vb_len, u32 ib_len, raw vb, raw ib}.
|
||||
{
|
||||
std::ofstream out(base_path.string() + ".buffers.bin", std::ios::binary);
|
||||
out.write("SK3BUFS1", 8);
|
||||
for (const RecordedBuffer& b : g_recorded_buffers) {
|
||||
const uint32_t vb_len = uint32_t(b.vb.size());
|
||||
const uint32_t ib_len = uint32_t(b.ib.size());
|
||||
out.write(reinterpret_cast<const char*>(&b.vb_addr), 4);
|
||||
out.write(reinterpret_cast<const char*>(&b.ib_addr), 4);
|
||||
out.write(reinterpret_cast<const char*>(&b.fingerprint), 8);
|
||||
out.write(reinterpret_cast<const char*>(&vb_len), 4);
|
||||
out.write(reinterpret_cast<const char*>(&ib_len), 4);
|
||||
out.write(reinterpret_cast<const char*>(b.vb.data()), vb_len);
|
||||
out.write(reinterpret_cast<const char*>(b.ib.data()), ib_len);
|
||||
}
|
||||
}
|
||||
|
||||
// Per-frame item dump.
|
||||
{
|
||||
std::ofstream out(base_path.string() + ".scene.jsonl");
|
||||
const auto write_item = [&out](const DrawItem& d) {
|
||||
out << "{\"mesh\":\"" << std::hex << d.mesh << "\",\"ib_obj\":\"" << d.ib_obj
|
||||
<< "\",\"vb_obj\":\"" << d.vb_obj << "\",\"vb_addr\":\"" << d.vb_addr
|
||||
<< "\",\"ib_addr\":\"" << d.ib_addr << "\",\"diffuse\":\"" << d.diffuse_tex
|
||||
<< "\",\"diffuse_fetch\":\"" << d.diffuse_fetch[1]
|
||||
<< "\",\"lightmap\":\"" << d.lightmap_tex << "\",\"macro\":\"" << d.macro_tex
|
||||
<< "\",\"decal_art\":\"" << d.decal_art
|
||||
<< "\",\"decal_fetch\":\"" << d.decal_fetch[1]
|
||||
<< "\",\"fp\":\"" << d.fingerprint
|
||||
<< "\"" << std::dec << ",\"vb_bytes\":" << d.vb_bytes << ",\"ib_count\":"
|
||||
<< d.ib_count << ",\"stride\":" << int(d.stride) << ",\"pos_fmt\":"
|
||||
<< int(d.pos_fmt) << ",\"pos_off\":" << d.pos_offset << ",\"bw_off\":"
|
||||
<< d.bw_offset << ",\"bi_off\":" << d.bi_offset << ",\"skinned\":"
|
||||
<< (d.skinned ? 1 : 0) << ",\"pending\":" << (d.pending ? 1 : 0)
|
||||
<< ",\"decal\":" << (d.decal ? 1 : 0)
|
||||
<< ",\"transparent\":" << (d.transparent ? 1 : 0)
|
||||
<< ",\"selected\":" << (d.selected ? 1 : 0) << ",\"world\":[";
|
||||
for (int i = 0; i < 16; ++i) out << (i ? "," : "") << d.world[i];
|
||||
out << "],\"draws\":[";
|
||||
for (size_t i = 0; i < d.draws.size(); ++i) {
|
||||
const DrawEntry& e = d.draws[i];
|
||||
out << (i ? "," : "") << "[" << e.prim << "," << e.base_vertex << ","
|
||||
<< e.start_index << "," << e.index_count << "]";
|
||||
}
|
||||
out << "],\"bones\":[";
|
||||
for (size_t i = 0; i < d.bones.size(); ++i) out << (i ? "," : "") << d.bones[i];
|
||||
out << "]}";
|
||||
};
|
||||
for (const RecordedFrame& rf : g_recorded_frames) {
|
||||
out << "{\"generation\":" << rf.generation << ",\"cam\":[" << rf.cam_pos[0] << ","
|
||||
<< rf.cam_pos[1] << "," << rf.cam_pos[2] << "],\"view_proj\":[";
|
||||
for (int i = 0; i < 16; ++i) out << (i ? "," : "") << rf.view_proj[i];
|
||||
out << "],\"dynitems\":[";
|
||||
for (size_t i = 0; i < rf.dynitems.size(); ++i) {
|
||||
if (i) out << ",";
|
||||
write_item(rf.dynitems[i]);
|
||||
}
|
||||
out << "],\"items\":[";
|
||||
for (size_t i = 0; i < rf.items.size(); ++i) {
|
||||
if (i) out << ",";
|
||||
write_item(rf.items[i]);
|
||||
}
|
||||
out << "]}\n";
|
||||
}
|
||||
}
|
||||
REXLOG_INFO(
|
||||
"native-scene: recording written ({} draws, {} frames, {} buffers {} MiB) -> "
|
||||
"{}.draws.bin/.scene.jsonl/.buffers.bin",
|
||||
g_recorded_draws.size(), g_recorded_frames.size(), g_recorded_buffers.size(),
|
||||
g_recorded_buffer_bytes >> 20, base_path.string());
|
||||
g_recorded_draws.clear();
|
||||
g_recorded_frames.clear();
|
||||
g_recorded_buffers.clear();
|
||||
g_recorded_buffer_keys.clear();
|
||||
g_recorded_buffer_bytes = 0;
|
||||
g_pending_inline_dumps.clear();
|
||||
g_frame_dump_ids.clear();
|
||||
}
|
||||
|
||||
// ---- Camera-signal recorder ---------------------------------------------------
|
||||
|
||||
namespace {
|
||||
struct CamSigEntry {
|
||||
double t; // host time (sampler push / frame build)
|
||||
double play_t; // kind 1 only: the smoother's playback time
|
||||
float yaw; // heading, degrees
|
||||
uint8_t kind; // 0 = raw sampler pose, 1 = smoothed frame, 2 = raw frame
|
||||
};
|
||||
std::mutex g_camsig_mutex;
|
||||
std::vector<CamSigEntry> g_camsig;
|
||||
} // namespace
|
||||
std::atomic<double> g_camsig_deadline{0.0};
|
||||
|
||||
float YawFromViewRows(const float view[16]) {
|
||||
return std::atan2(view[2], view[10]) * float(180.0 / 3.14159265358979323846);
|
||||
}
|
||||
|
||||
void CamSigPush(double t, double play_t, float yaw, uint8_t kind) {
|
||||
std::lock_guard<std::mutex> lock(g_camsig_mutex);
|
||||
if (g_camsig.size() < 200000) {
|
||||
g_camsig.push_back({t, play_t, yaw, kind});
|
||||
}
|
||||
}
|
||||
|
||||
void CamSigFrameTick(double rec_now, const float cam_view[16],
|
||||
const float* smoothed_rot, double play_t) {
|
||||
const double dl = g_camsig_deadline.load(std::memory_order_relaxed);
|
||||
if (dl <= 0.0) {
|
||||
return;
|
||||
}
|
||||
if (rec_now < dl) {
|
||||
std::lock_guard<std::mutex> lock(g_camsig_mutex);
|
||||
if (g_camsig.size() < 200000) {
|
||||
g_camsig.push_back({rec_now, 0.0, YawFromViewRows(cam_view), 2});
|
||||
if (smoothed_rot) {
|
||||
g_camsig.push_back({rec_now, play_t, YawFromViewRows(smoothed_rot), 1});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std::vector<CamSigEntry> entries;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_camsig_mutex);
|
||||
entries.swap(g_camsig);
|
||||
}
|
||||
g_camsig_deadline.store(0.0, std::memory_order_release);
|
||||
std::thread([entries = std::move(entries)]() {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories("logs", ec);
|
||||
char path[128];
|
||||
std::snprintf(path, sizeof(path), "logs/cam_signal_%lld.csv",
|
||||
static_cast<long long>(std::time(nullptr)));
|
||||
std::ofstream f(path);
|
||||
f << "kind,t,play_t,yaw_deg\n";
|
||||
char line[128];
|
||||
for (const CamSigEntry& e : entries) {
|
||||
std::snprintf(line, sizeof(line), "%d,%.6f,%.6f,%.5f\n", int(e.kind), e.t,
|
||||
e.play_t, double(e.yaw));
|
||||
f << line;
|
||||
}
|
||||
REXLOG_INFO("native-scene cam-signal: wrote {} entries -> {}", entries.size(),
|
||||
path);
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
|
||||
void RecordCameraSignal(double seconds) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_camsig_mutex);
|
||||
g_camsig.clear();
|
||||
g_camsig.reserve(size_t(seconds * 1400.0));
|
||||
}
|
||||
const double now = std::chrono::duration<double>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
g_camsig_deadline.store(now + seconds, std::memory_order_release);
|
||||
REXLOG_INFO(
|
||||
"native-scene cam-signal: recording {} s; pan the camera with the stick at a "
|
||||
"steady rate NOW",
|
||||
seconds);
|
||||
}
|
||||
|
||||
// ---- Bone-signal recorder -----------------------------------------------------
|
||||
|
||||
namespace {
|
||||
std::vector<uint8_t> g_bonesig;
|
||||
std::atomic<double> g_bonesig_deadline{0.0};
|
||||
// Armed from the UI thread (F12 button), consumed on the guest thread; the
|
||||
// blob itself is guest-thread-only.
|
||||
std::atomic<double> g_bonesig_request{0.0};
|
||||
} // namespace
|
||||
|
||||
void BoneSigAppend(uint8_t kind, uint64_t key, double t, double play, const float* v,
|
||||
uint32_t n) {
|
||||
if (g_bonesig.size() > (200u << 20)) {
|
||||
return; // runaway cap
|
||||
}
|
||||
const size_t off = g_bonesig.size();
|
||||
g_bonesig.resize(off + 29 + size_t(n) * 4);
|
||||
uint8_t* p = g_bonesig.data() + off;
|
||||
*p = kind;
|
||||
std::memcpy(p + 1, &key, 8);
|
||||
std::memcpy(p + 9, &t, 8);
|
||||
std::memcpy(p + 17, &play, 8);
|
||||
std::memcpy(p + 25, &n, 4);
|
||||
std::memcpy(p + 29, v, size_t(n) * 4);
|
||||
}
|
||||
|
||||
bool BoneSigTick(double now) {
|
||||
const double req = g_bonesig_request.exchange(0.0, std::memory_order_acq_rel);
|
||||
if (req > 0.0) {
|
||||
g_bonesig.clear();
|
||||
g_bonesig.reserve(16u << 20);
|
||||
g_bonesig_deadline.store(now + req, std::memory_order_relaxed);
|
||||
}
|
||||
const double dl = g_bonesig_deadline.load(std::memory_order_relaxed);
|
||||
if (dl <= 0.0) {
|
||||
return false;
|
||||
}
|
||||
if (now < dl) {
|
||||
return true;
|
||||
}
|
||||
std::vector<uint8_t> blob;
|
||||
blob.swap(g_bonesig);
|
||||
g_bonesig_deadline.store(0.0, std::memory_order_release);
|
||||
std::thread([blob = std::move(blob)]() {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories("logs", ec);
|
||||
char path[128];
|
||||
std::snprintf(path, sizeof(path), "logs/bone_signal_%lld.bin",
|
||||
static_cast<long long>(std::time(nullptr)));
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
f.write("BSIG1\n", 6);
|
||||
f.write(reinterpret_cast<const char*>(blob.data()), std::streamsize(blob.size()));
|
||||
REXLOG_INFO("native-scene bone-signal: wrote {} bytes -> {}", blob.size() + 6,
|
||||
path);
|
||||
}).detach();
|
||||
return false;
|
||||
}
|
||||
|
||||
void RecordBoneSignal(double seconds) {
|
||||
g_bonesig_request.store(seconds, std::memory_order_release);
|
||||
REXLOG_INFO(
|
||||
"native-scene bone-signal: recording {} s; skate past the camera at a steady "
|
||||
"speed NOW",
|
||||
seconds);
|
||||
}
|
||||
|
||||
// ---- Synthetic camera pan -----------------------------------------------------
|
||||
|
||||
std::atomic<int> g_synpan_active{0}; // engaged mode (0 = off)
|
||||
std::mutex g_synpan_mutex;
|
||||
float g_synpan_view0[16]; // raw guest view at engage
|
||||
float g_synpan_proj0[16];
|
||||
float g_synpan_c0[3]; // camera world position at engage (held fixed)
|
||||
double g_synpan_t0 = 0.0;
|
||||
double g_synpan_step_phase = 0.0; // mode 2: accumulated phase (degrees)
|
||||
double g_synpan_ema_dt = 0.0; // mode 2: slow EMA of the publish dt
|
||||
uint64_t g_synpan_frames = 0;
|
||||
double g_synpan_last_build = 0.0;
|
||||
double g_synpan_dt_sum = 0.0, g_synpan_dt_sum2 = 0.0;
|
||||
double g_synpan_dt_min = 0.0, g_synpan_dt_max = 0.0;
|
||||
double g_synpan_err_sum2 = 0.0, g_synpan_err_max = 0.0;
|
||||
uint64_t g_synpan_err_n = 0;
|
||||
std::unordered_map<uint64_t, DrawItem> g_synpan_union;
|
||||
|
||||
uint64_t SynPanItemKey(const DrawItem& it) {
|
||||
uint64_t h = 1469598103934665603ull;
|
||||
const auto mix = [&h](uint64_t v) {
|
||||
h ^= v;
|
||||
h *= 1099511628211ull;
|
||||
};
|
||||
mix(it.mesh);
|
||||
mix(it.vb_obj);
|
||||
mix(it.ib_obj);
|
||||
mix(std::bit_cast<uint32_t>(it.world[12]));
|
||||
mix(std::bit_cast<uint32_t>(it.world[13]));
|
||||
mix(std::bit_cast<uint32_t>(it.world[14]));
|
||||
return h;
|
||||
}
|
||||
|
||||
double SynPanAngleDeg(double phase_deg, double amp) {
|
||||
if (amp <= 0.0) {
|
||||
return phase_deg;
|
||||
}
|
||||
const double period = 4.0 * amp;
|
||||
double m = std::fmod(phase_deg, period);
|
||||
if (m < 0.0) {
|
||||
m += period;
|
||||
}
|
||||
return m < amp ? m : (m < 3.0 * amp ? 2.0 * amp - m : m - period);
|
||||
}
|
||||
|
||||
void SynPanView(double angle_deg, float view_out[16]) {
|
||||
const double a = angle_deg * (3.14159265358979323846 / 180.0);
|
||||
const float c = float(std::cos(a)), s = float(std::sin(a));
|
||||
const float ry[3][3] = {{c, 0.0f, -s}, {0.0f, 1.0f, 0.0f}, {s, 0.0f, c}};
|
||||
std::memset(view_out, 0, 16 * sizeof(float));
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
float sum = 0.0f;
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
sum += ry[i][k] * g_synpan_view0[k * 4 + j];
|
||||
}
|
||||
view_out[i * 4 + j] = sum;
|
||||
}
|
||||
}
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
view_out[12 + k] = -(g_synpan_c0[0] * view_out[0 * 4 + k] +
|
||||
g_synpan_c0[1] * view_out[1 * 4 + k] +
|
||||
g_synpan_c0[2] * view_out[2 * 4 + k]);
|
||||
}
|
||||
view_out[15] = 1.0f;
|
||||
}
|
||||
|
||||
} // namespace skate3::native_scene
|
||||
256
src/native/skate3_native_guest_read.cpp
Normal file
256
src/native/skate3_native_guest_read.cpp
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
// Shared sample-vertex decode + skin + spread helpers (see the header).
|
||||
// Extracted verbatim from the five open-coded copies in
|
||||
// skate3_native_scene.cpp, behavior-preserving.
|
||||
|
||||
#include "native/skate3_native_guest_read.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
#include "generated/skate3_init.h"
|
||||
|
||||
namespace skate3::native_scene {
|
||||
|
||||
float GuestHalfToFloat(uint16_t h) {
|
||||
const uint32_t sign = uint32_t(h & 0x8000u) << 16;
|
||||
const uint32_t exp = (h >> 10) & 0x1F;
|
||||
const uint32_t man = h & 0x3FF;
|
||||
if (exp == 0) return std::bit_cast<float>(sign); // denorms ~0
|
||||
if (exp == 31) return std::bit_cast<float>(sign | 0x7F800000u);
|
||||
return std::bit_cast<float>(sign | ((exp + 112) << 23) | (man << 13));
|
||||
}
|
||||
|
||||
float BindDiag(const DrawItem& item) {
|
||||
float d2 = 0.0f;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
const float d = item.bbox_max[a] - item.bbox_min[a];
|
||||
d2 += d * d;
|
||||
}
|
||||
return std::sqrt(d2);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool PosFinite(const float p[3]) {
|
||||
return p[0] > -1e7f && p[0] < 1e7f && p[1] > -1e7f && p[1] < 1e7f &&
|
||||
p[2] > -1e7f && p[2] < 1e7f;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ReadSkinVertGuest(uint8_t* base, const DrawItem& item, uint32_t vtx,
|
||||
SkinSampleVert* out) {
|
||||
const uint32_t v = item.vb_addr + vtx * item.stride;
|
||||
const uint32_t pa = v + item.pos_offset;
|
||||
float* p = out->p;
|
||||
switch (item.pos_fmt) {
|
||||
case 57:
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
const uint32_t u = REX_LOAD_U32(pa + a * 4);
|
||||
std::memcpy(&p[a], &u, 4);
|
||||
}
|
||||
break;
|
||||
case 32:
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
p[a] = GuestHalfToFloat(uint16_t(REX_LOAD_U16(pa + a * 2)));
|
||||
}
|
||||
break;
|
||||
case 26: {
|
||||
constexpr float kScale = 2.0f / 32767.0f;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
p[a] = int16_t(REX_LOAD_U16(pa + a * 2)) * kScale + (a == 1 ? 0.8f : 0.0f);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
out->pos_finite = PosFinite(p);
|
||||
if (item.bw_offset != 0 && item.bi_offset != 0) {
|
||||
// u8x4 attributes are big-endian per 32-bit word: component k is byte
|
||||
// (24 - 8k) of the host-order load.
|
||||
const uint32_t bw = REX_LOAD_U32(v + item.bw_offset);
|
||||
const uint32_t bi = REX_LOAD_U32(v + item.bi_offset);
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
out->w[k] = uint8_t((bw >> (24 - 8 * k)) & 0xFF);
|
||||
out->bone[k] = uint8_t((bi >> (24 - 8 * k)) & 0xFF);
|
||||
}
|
||||
} else {
|
||||
std::memset(out->w, 0, sizeof(out->w));
|
||||
std::memset(out->bone, 0, sizeof(out->bone));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadSkinSamplesGuest(uint8_t* base, const DrawItem& item, uint32_t n,
|
||||
SkinSampleVert* out) {
|
||||
if (item.stride == 0) {
|
||||
return false;
|
||||
}
|
||||
const uint32_t count = item.vb_bytes / item.stride;
|
||||
if (count < 2 || n < 2) {
|
||||
return false;
|
||||
}
|
||||
for (uint32_t s = 0; s < n; ++s) {
|
||||
if (!ReadSkinVertGuest(base, item, s * (count - 1) / (n - 1), &out[s])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int ReadSkinSamplesRaw(const uint8_t* vb, size_t vb_size, const DrawItem& item,
|
||||
uint32_t n, SkinSampleVert* out) {
|
||||
if (item.stride == 0) {
|
||||
return -1;
|
||||
}
|
||||
const uint32_t count = item.vb_bytes / item.stride;
|
||||
if (count < 2 || n < 2) {
|
||||
return -1;
|
||||
}
|
||||
for (uint32_t s = 0; s < n; ++s) {
|
||||
const uint32_t v = (s * (count - 1) / (n - 1)) * item.stride;
|
||||
if (uint64_t(v) + item.stride > vb_size) {
|
||||
return int(s); // short read: caller treats as nothing-to-judge
|
||||
}
|
||||
const uint8_t* vp = vb + v;
|
||||
SkinSampleVert& sv = out[s];
|
||||
float* p = sv.p;
|
||||
// Guest payload copied raw = big-endian attributes.
|
||||
switch (item.pos_fmt) {
|
||||
case 57:
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
const uint8_t* b = vp + item.pos_offset + a * 4;
|
||||
const uint32_t w = uint32_t(b[0]) << 24 | uint32_t(b[1]) << 16 |
|
||||
uint32_t(b[2]) << 8 | b[3];
|
||||
p[a] = std::bit_cast<float>(w);
|
||||
}
|
||||
break;
|
||||
case 32:
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
const uint8_t* b = vp + item.pos_offset + a * 2;
|
||||
p[a] = GuestHalfToFloat(uint16_t(uint16_t(b[0]) << 8 | b[1]));
|
||||
}
|
||||
break;
|
||||
case 26: {
|
||||
constexpr float kScale = 2.0f / 32767.0f;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
const uint8_t* b = vp + item.pos_offset + a * 2;
|
||||
p[a] = int16_t(uint16_t(b[0]) << 8 | b[1]) * kScale + (a == 1 ? 0.8f : 0.0f);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
sv.pos_finite = PosFinite(p);
|
||||
if (item.bw_offset != 0 && item.bi_offset != 0) {
|
||||
// Component k = guest byte k (matches the live path's
|
||||
// byte-(24-8k)-of-host-load convention).
|
||||
const uint8_t* bw = vp + item.bw_offset;
|
||||
const uint8_t* bi = vp + item.bi_offset;
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
sv.w[k] = bw[k];
|
||||
sv.bone[k] = bi[k];
|
||||
}
|
||||
} else {
|
||||
std::memset(sv.w, 0, sizeof(sv.w));
|
||||
std::memset(sv.bone, 0, sizeof(sv.bone));
|
||||
}
|
||||
}
|
||||
return int(n);
|
||||
}
|
||||
|
||||
uint32_t SkinPointHostRows(const SkinSampleVert& sv, const float* rows,
|
||||
size_t rows_floats, float q[3]) {
|
||||
uint32_t total = 0;
|
||||
q[0] = q[1] = q[2] = 0.0f;
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
const uint32_t w = sv.w[k];
|
||||
if (w == 0) continue;
|
||||
const uint32_t r0 = 3u * sv.bone[k];
|
||||
if ((r0 + 3) * 4 > rows_floats) continue;
|
||||
total += w;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
const float* row = rows + (r0 + uint32_t(a)) * 4;
|
||||
q[a] += float(w) * (row[0] * sv.p[0] + row[1] * sv.p[1] + row[2] * sv.p[2] +
|
||||
row[3]);
|
||||
}
|
||||
}
|
||||
if (total != 0) {
|
||||
for (int a = 0; a < 3; ++a) q[a] /= float(total);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
uint32_t SkinPointBankRows(uint8_t* base, uint32_t bank, uint32_t pb,
|
||||
const SkinSampleVert& sv, float q[3], bool* rows_sane) {
|
||||
uint32_t total = 0;
|
||||
q[0] = q[1] = q[2] = 0.0f;
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
const uint32_t w = sv.w[k];
|
||||
if (w == 0) continue;
|
||||
const uint32_t r0 = pb + 3 * uint32_t(sv.bone[k]);
|
||||
if (r0 + 3 > 256) continue;
|
||||
total += w;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
float row[4];
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
row[i] = std::bit_cast<float>(REX_LOAD_U32(bank + ((r0 + a) * 4 + i) * 4));
|
||||
}
|
||||
// A weighted bone's rotation row must be rotation-shaped (norm near
|
||||
// the entity scale). The vehicle-flick captures carried WORLD
|
||||
// POSITIONS in the rotation slots (norm ~140); those can still
|
||||
// project on-screen and pass the geometric gate.
|
||||
const float rn = row[0] * row[0] + row[1] * row[1] + row[2] * row[2];
|
||||
if (rn < 0.04f || rn > 25.0f) {
|
||||
*rows_sane = false;
|
||||
}
|
||||
q[a] += float(w) * (row[0] * sv.p[0] + row[1] * sv.p[1] + row[2] * sv.p[2] +
|
||||
row[3]);
|
||||
}
|
||||
}
|
||||
if (total != 0) {
|
||||
for (int a = 0; a < 3; ++a) q[a] /= float(total);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
int SkinnedSpreadHostRows(const SkinSampleVert* sv, uint32_t n, const float* rows,
|
||||
size_t rows_floats, int min_n, bool garbage_fails,
|
||||
float* out_spread, int* out_n) {
|
||||
float qmin[3] = {1e9f, 1e9f, 1e9f};
|
||||
float qmax[3] = {-1e9f, -1e9f, -1e9f};
|
||||
int weighted = 0;
|
||||
for (uint32_t s = 0; s < n; ++s) {
|
||||
if (!sv[s].pos_finite) {
|
||||
if (garbage_fails) {
|
||||
return -1; // NaN/garbage positions mid-sim-write
|
||||
}
|
||||
continue;
|
||||
}
|
||||
float q[3];
|
||||
if (SkinPointHostRows(sv[s], rows, rows_floats, q) == 0) {
|
||||
continue;
|
||||
}
|
||||
++weighted;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
qmin[a] = std::min(qmin[a], q[a]);
|
||||
qmax[a] = std::max(qmax[a], q[a]);
|
||||
}
|
||||
}
|
||||
if (out_n) {
|
||||
*out_n = weighted;
|
||||
}
|
||||
if (weighted < min_n) {
|
||||
return 0; // nothing to judge
|
||||
}
|
||||
const float dx = qmax[0] - qmin[0];
|
||||
const float dy = qmax[1] - qmin[1];
|
||||
const float dz = qmax[2] - qmin[2];
|
||||
*out_spread = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace skate3::native_scene
|
||||
86
src/native/skate3_native_guest_read.h
Normal file
86
src/native/skate3_native_guest_read.h
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
#pragma once
|
||||
|
||||
// Shared sample-vertex decode + skin + spread helpers for the native scene
|
||||
// renderer: the ONE copy of the decode+skin loop that was open-coded five
|
||||
// times (RefinePaletteBase, ScoreRigidAffine, RopaPayloadCoherent,
|
||||
// PublishedPaletteSane, the InterpolateDynamicItems centroid pass).
|
||||
//
|
||||
// Decode conventions unified here (verified against the original sites):
|
||||
// - guest u8x4 blend attribs are big-endian per 32-bit word: influence k is
|
||||
// guest byte k, i.e. byte (24 - 8k) of the host-order load;
|
||||
// - position fmt 57 = float3, 32 = half4 (xyz), 26 = s16x4 with scale
|
||||
// 2/32767 and the +0.8 Y bias;
|
||||
// - a raw copied payload holds the same guest bytes (big-endian).
|
||||
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "skate3_native_scene.h"
|
||||
|
||||
namespace skate3::native_scene {
|
||||
|
||||
// One decoded vertex: bind-space position + u8x4 blend attribs (influence k
|
||||
// = w[k]/bone[k]; all zero when the item carries no blend attributes).
|
||||
struct SkinSampleVert {
|
||||
float p[3];
|
||||
uint8_t w[4];
|
||||
uint8_t bone[4];
|
||||
bool pos_finite; // every |p| < 1e7 (mid-sim-write garbage detection)
|
||||
};
|
||||
|
||||
float GuestHalfToFloat(uint16_t h);
|
||||
|
||||
// Bind-pose bbox diagonal of the item: the reference size every
|
||||
// spread-vs-bind coherence gate compares against.
|
||||
float BindDiag(const DrawItem& item);
|
||||
|
||||
// Decode vertex `vtx` (index) of the item's LIVE guest VB. Blend attribs are
|
||||
// read only when both offsets are non-zero. Returns false on an unsupported
|
||||
// position format.
|
||||
bool ReadSkinVertGuest(uint8_t* base, const DrawItem& item, uint32_t vtx,
|
||||
SkinSampleVert* out);
|
||||
|
||||
// Decode `n` evenly spaced sample verts (index s*(count-1)/(n-1)) of the
|
||||
// item's LIVE guest VB. Returns false when the position format is
|
||||
// unsupported or the VB holds fewer than 2 vertices.
|
||||
bool ReadSkinSamplesGuest(uint8_t* base, const DrawItem& item, uint32_t n,
|
||||
SkinSampleVert* out);
|
||||
|
||||
// Same sampling from a COPIED raw payload (big-endian guest bytes). Sample
|
||||
// addressing uses the item's stride/count (item.vb_bytes), bounds-checked
|
||||
// against the copy: returns the number of samples decoded before the first
|
||||
// out-of-bounds sample (callers treat a short read as "nothing to judge"),
|
||||
// or -1 when the position format is unsupported / count < 2.
|
||||
int ReadSkinSamplesRaw(const uint8_t* vb, size_t vb_size, const DrawItem& item,
|
||||
uint32_t n, SkinSampleVert* out);
|
||||
|
||||
// Weighted [R | t] skin with HOST palette rows (DrawItem::bones layout:
|
||||
// 3 float4 rows per bone, column-vector affine). Influences whose rows fall
|
||||
// outside the palette are skipped. Returns the total u8 weight (0 = nothing
|
||||
// weighted); q is the weight-normalized skinned position when total > 0.
|
||||
uint32_t SkinPointHostRows(const SkinSampleVert& sv, const float* rows,
|
||||
size_t rows_floats, float q[3]);
|
||||
|
||||
// Weighted skin with the palette staged in a guest VS constant bank at base
|
||||
// register pb (3 rows per bone). Influences past register 256 are skipped.
|
||||
// *rows_sane is CLEARED when any weighted rotation row's xyz norm falls
|
||||
// outside [0.04, 25]; world positions in rotation slots can still project
|
||||
// on-screen and pass the geometric gates.
|
||||
uint32_t SkinPointBankRows(uint8_t* base, uint32_t bank, uint32_t pb,
|
||||
const SkinSampleVert& sv, float q[3], bool* rows_sane);
|
||||
|
||||
// Spread (bbox diagonal) of the decoded samples skinned with host palette
|
||||
// rows: the shared core of the three coherence gates. Samples with a
|
||||
// non-finite position either fail the gate (garbage_fails, the frame-end
|
||||
// ropa check: mid-sim-write garbage means incoherent) or are skipped (the
|
||||
// publish-time gate). Returns:
|
||||
// -1 = garbage position encountered (garbage_fails only),
|
||||
// 0 = fewer than min_n weighted samples (nothing to judge),
|
||||
// 1 = *out_spread measured.
|
||||
// *out_n (optional) receives the weighted-sample count.
|
||||
int SkinnedSpreadHostRows(const SkinSampleVert* sv, uint32_t n, const float* rows,
|
||||
size_t rows_floats, int min_n, bool garbage_fails,
|
||||
float* out_spread, int* out_n = nullptr);
|
||||
|
||||
} // namespace skate3::native_scene
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue