render: graphics build-up showcase mode

This commit is contained in:
mchughalex 2026-07-19 19:58:30 +01:00
parent 38343207ff
commit ccf84fade3
12 changed files with 15943 additions and 12638 deletions

View file

@ -360,9 +360,21 @@ float4 ps_bloom_up(VSOut i) : SV_Target {
// upsample; the white fallback rides here with intensity 0 when bloom is // upsample; the white fallback rides here with intensity 0 when bloom is
// off). // off).
float4 ps_tonemap(VSOut i) : SV_Target { float4 ps_tonemap(VSOut i) : SV_Target {
// Graphics build-up showcase (see scene.hlsl ShowcaseMask): sh_v2.y/z =
// 256 + the layer mask left/right of the split at sh_v2.w (output
// pixels), 0 = off. Post layers reveal per pixel by their mask bits:
// AO 16, volumetrics 64, bloom 128 (SSR gates in its own composite).
float scv = i.pos.x < sh_v2.w ? sh_v2.y : sh_v2.z;
int sc_mask = scv < 255.5 ? -1 : (int)(scv + 0.5) - 256;
float ao = ao_plane.SampleLevel(smp_linear, i.uv, 0); float ao = ao_plane.SampleLevel(smp_linear, i.uv, 0);
if (sc_mask >= 0 && (sc_mask & 16) == 0) {
ao = 1.0;
}
float3 x = max(tex0.SampleLevel(smp_point, i.uv, 0).rgb, 0.0) * ao; float3 x = max(tex0.SampleLevel(smp_point, i.uv, 0).rgb, 0.0) * ao;
float3 bloom = tex1.SampleLevel(smp_linear, i.uv, 0).rgb * p0.z; float3 bloom = tex1.SampleLevel(smp_linear, i.uv, 0).rgb * p0.z;
if (sc_mask >= 0 && (sc_mask & 128) == 0) {
bloom = 0.0;
}
// Volumetric terms, applied AFTER the AO multiply (in-air light is not // Volumetric terms, applied AFTER the AO multiply (in-air light is not
// subject to surface occlusion). The shaft plane carries the marched // subject to surface occlusion). The shaft plane carries the marched
// SHADOWED-air term (see ps_vol_shafts) and dims MULTIPLICATIVELY; // SHADOWED-air term (see ps_vol_shafts) and dims MULTIPLICATIVELY;
@ -376,7 +388,8 @@ float4 ps_tonemap(VSOut i) : SV_Target {
// across depth discontinuities) so shaft edges never bleed onto near // across depth discontinuities) so shaft edges never bleed onto near
// silhouettes. // silhouettes.
float shaft = 0.0; float shaft = 0.0;
if (vs0.w > 0.0) { bool sc_vol = sc_mask < 0 || (sc_mask & 64) != 0;
if (vs0.w > 0.0 && sc_vol) {
float dpix = lin_depth.SampleLevel(smp_point, i.uv, 0); float dpix = lin_depth.SampleLevel(smp_point, i.uv, 0);
float2 vt = size.zw * 2.0; // one shaft-plane texel in uv float2 vt = size.zw * 2.0; // one shaft-plane texel in uv
float2 base = (floor(i.uv / vt - 0.5) + 0.5) * vt; float2 base = (floor(i.uv / vt - 0.5) + 0.5) * vt;
@ -400,7 +413,7 @@ float4 ps_tonemap(VSOut i) : SV_Target {
// value only; the game's own fog already renders the isotropic part, // value only; the game's own fog already renders the isotropic part,
// so adding a floor here just veils the whole frame. // so adding a floor here just veils the whole frame.
float3 haze = float3(0.0, 0.0, 0.0); float3 haze = float3(0.0, 0.0, 0.0);
if (vs1.w > 0.0) { if (vs1.w > 0.0 && sc_vol) {
float d = lin_depth.SampleLevel(smp_point, i.uv, 0); float d = lin_depth.SampleLevel(smp_point, i.uv, 0);
float path = 1.0 - exp(-max(vs2.w, 0.0005) * d); float path = 1.0 - exp(-max(vs2.w, 0.0005) * d);
float3 ray = normalize(ViewPos(i.uv, 1.0)); float3 ray = normalize(ViewPos(i.uv, 1.0));
@ -431,6 +444,15 @@ float4 ps_tonemap(VSOut i) : SV_Target {
// Dimming ceiling: shadowed air thins the light, it does not extinguish // Dimming ceiling: shadowed air thins the light, it does not extinguish
// it; without the ceiling a fully shaded sun-facing view (under an // it; without the ceiling a fully shaded sun-facing view (under an
// overpass) saturated to a solid black void. // overpass) saturated to a solid black void.
return float4( float3 outc =
ToneMapScene((x + haze) * (1.0 - 0.55 * saturate(shaft)) + bloom), 1.0); ToneMapScene((x + haze) * (1.0 - 0.55 * saturate(shaft)) + bloom);
// Showcase divider: a soft white line at the split while a wipe is in
// flight (the two sides show different stages).
if (sh_v2.w > 0.5 && abs(sh_v2.y - sh_v2.z) > 0.5) {
float half_w = max(size.x * 0.0012, 1.5);
float d = abs(i.pos.x - sh_v2.w);
outc = lerp(outc, float3(1.0, 1.0, 1.0),
saturate(1.0 - d / half_w) * 0.85);
}
return float4(outc, 1.0);
} }

View file

@ -135,12 +135,30 @@ VSOut vs_main(float3 p : POSITION, float2 uv : TEXCOORD0, float2 uv2 : TEXCOORD1
: float4(mul(sb, (float3x3)world), bw.w); : float4(mul(sb, (float3x3)world), bw.w);
return o; return o;
} }
// ---- Graphics build-up showcase -------------------------------------------
// sh_v2.yzw carry the showcase split state: y/z = the LAYER MASK shown
// left/right of the vertical split at w (in output pixels), encoded as
// 256 + mask so an all-zero row (showcase off) is unambiguous. Mask bits
// (the contract shared with hdr.hlsl, ssr.hlsl and the CPU sequencer,
// see kShowcaseLayers in skate3_native_scene.h):
// 1 albedo textures 2 baked lighting 4 materials & surface detail
// 8 dynamic shadows 16 ambient occlusion 32 reflections (SSR)
// 64 volumetrics 128 bloom
// The material bits are progressive looks (materials subsumes lighting
// subsumes albedo); the rest are independent, so the sequencer can reveal
// them in any order or grouping. Returns -1 when the showcase is off.
int ShowcaseMask(float px_x) {
float v = px_x < sh_v2.w ? sh_v2.y : sh_v2.z;
return v < 255.5 ? -1 : (int)(v + 0.5) - 256;
}
#include "scene_shadows.hlsli" #include "scene_shadows.hlsli"
#include "scene_common.hlsli" #include "scene_common.hlsli"
#include "scene_char.hlsli" #include "scene_char.hlsli"
#include "scene_dynobj.hlsli" #include "scene_dynobj.hlsli"
#include "scene_water.hlsli" #include "scene_water.hlsli"
// The full material shading for every family (ps_main forwards here). // The full material shading for every family (see ps_main below; the
// showcase wrapper substitutes the early build-up stages' looks over this
// result's rgb while keeping its alpha).
float4 ShadePixel(VSOut i) { float4 ShadePixel(VSOut i) {
if (tint.a > 0.0) { if (tint.a > 0.0) {
return float4(PassGamma(tint.rgb), tint.a); return float4(PassGamma(tint.rgb), tint.a);
@ -679,7 +697,88 @@ float4 ShadePixel(VSOut i) {
return float4(PassGamma(lit), 1.0); return float4(PassGamma(lit), 1.0);
} }
float4 ps_main(VSOut i) : SV_Target { float4 ps_main(VSOut i) : SV_Target {
return ShadePixel(i); float4 c = ShadePixel(i);
int mask = ShowcaseMask(i.pos.x);
if (mask < 0 || (mask & 4) != 0) {
return c; // showcase off, or the full material layer is revealed
}
// Pre-material build-up looks replace the shaded rgb but keep the
// branch's own alpha, so alpha tests, coverage channels, fades and blend
// routing all behave exactly as in the full render. The gamma-space
// looks encode through PassGamma like the legacy branches; the exact-
// chain lighting look goes through ToneOut. The sky dome (exact family
// -40 / the legacy unlit marker) is authored content; it arrives
// complete with the lighting layer and never receives cast shadows.
bool sky = (cam_pos.w < -39.5 && cam_pos.w > -40.5) || tint.b > 0.0;
if (sky && (mask & 2) != 0) {
return c;
}
float3 n = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
float3 ldir = dot(sh_sun.xyz, sh_sun.xyz) > 0.1
? sh_sun.xyz
: normalize(float3(0.4, 0.8, 0.3));
// The dynamic-shadow layer applies to every pre-material look (the
// helpers self-gate on the mask, so uncovered frames sample lit).
float s = 1.0;
if (!sky && (mask & 8) != 0) {
s = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz,
tint.g > 0.0 ? 0.012 : 0.0, i.nrm, i.pos.xy),
SampleStaticSun(i.rpos + cam_pos.xyz, i.nrm, i.pos.xy));
}
// Base color: the diffuse texture once the albedo layer is revealed,
// clay grey before it.
float3 base = (mask & 1) != 0 ? diffuse.Sample(smp, i.uv).rgb
: float3(0.62, 0.62, 0.62);
if ((mask & 2) != 0 && tint.r > 0.0 && sh_sun.w > 0.01) {
// Baked lighting on a lightmapped surface: the REAL diffuse chain,
// lightmap^2 x the flat-map kd x fog/exposure/material multiplier,
// the exact-family formula minus surface detail, so the later step
// to full materials only adds normal/detail maps, specular,
// reflections and weathering instead of also re-grading the frame's
// tone. The shadow layer joins with the game's own lightmap min-clamp.
float3 lm = lightmap.SampleLevel(smp_clamp, i.uv2, 0.0).rgb;
float3 lml = lm * lm;
if ((mask & 8) != 0) {
lml = min(lml, s + sh_color.rgb);
}
float3 lin = lml * 0.93429 * base * base;
float fdist = length(i.rpos);
float f1 = saturate(fdist * sh_fogp.x + sh_fogp.y);
if (sh_fogp.z != 1.0) {
f1 = pow(max(f1, 1e-6), sh_fogp.z);
}
float3 xe = (lin * ((1.0 + sh_fogc.a * f1) * sh_env.x) +
sh_fogc.rgb * f1) * sh_sun.w;
return ToneOut(xe, c.a, false);
}
float3 col;
if ((mask & 2) != 0) {
// Lighting without exact frame rows: the legacy-branch convention
// (lightmap x2 in gamma space), or a neutral wrapped lambert for
// surfaces without a lightmap (characters, props).
float3 lite;
if (tint.r > 0.0) {
lite = lightmap.SampleLevel(smp_clamp, i.uv2, 0.0).rgb * 2.0;
} else {
float wrap = 0.35 + 0.65 * saturate(dot(n, ldir));
lite = float3(wrap, wrap, wrap);
}
col = base * lite;
} else if ((mask & 1) != 0) {
col = base; // raw diffuse, unlit
} else {
// Clay: grey with a simple sun-lambert so the geometry reads (a fully
// flat grey collapses everything into silhouettes).
col = base * (0.61 + 0.68 * saturate(dot(n, ldir)));
}
// Shadow layer on the gamma-space looks: a soft darkening multiply (the
// exact-chain look above uses the game's own clamp instead).
if (!sky && (mask & 8) != 0) {
col *= s * 0.65 + 0.35;
}
return float4(PassGamma(saturate(col)), c.a);
} }
// ---- SSR reflection G-buffer (consumed by ssr.hlsl ps_march) -------------- // ---- SSR reflection G-buffer (consumed by ssr.hlsl ps_march) --------------
// Rendered after the main pass from the frame's reflective items only (env // Rendered after the main pass from the frame's reflective items only (env

View file

@ -1,8 +1,8 @@
// Shadow receivers: the game-exact CSM atlas sampler, its // Shadow receivers: the game-exact CSM atlas sampler, its
// contact-hardening PCSS variant, and the native static sun-shadow map // contact-hardening PCSS variant, and the native static sun-shadow map
// sampler. Expects the including file to declare the b1 rows // sampler. Expects the including file to declare the b1 rows
// (scene_frame_cb.hlsli) and the shadow_atlas / static_sun textures // (scene_frame_cb.hlsli), the shadow_atlas / static_sun textures and
// before this include. // ShowcaseMask before this include.
// Native CSM atlas sample at a world position: finest covering cascade, // Native CSM atlas sample at a world position: finest covering cascade,
// s = saturate(infront + 1 - coverage). Returns 1 (lit) when uncovered or // s = saturate(infront + 1 - coverage). Returns 1 (lit) when uncovered or
// shadows are off. extra_bias suppresses receiver self-shadow acne on // shadows are off. extra_bias suppresses receiver self-shadow acne on
@ -120,6 +120,11 @@ float PcssRotation(float2 px) {
} }
float SampleCsmShadowSoft(float3 wp, float extra_bias, float3 nrm, float SampleCsmShadowSoft(float3 wp, float extra_bias, float3 nrm,
float2 px) { float2 px) {
// Showcase: until the shadow layer is revealed, render fully lit.
int sc_mask = ShowcaseMask(px.x);
if (sc_mask >= 0 && (sc_mask & 8) == 0) {
return 1.0;
}
if (sh_pcss.x <= 0.5) { if (sh_pcss.x <= 0.5) {
return SampleCsmShadow(wp, extra_bias); return SampleCsmShadow(wp, extra_bias);
} }
@ -235,6 +240,11 @@ float SampleStaticSun(float3 wp, float3 nrm, float2 px) {
if (nsm_p.x <= 0.0) { if (nsm_p.x <= 0.0) {
return 1.0; return 1.0;
} }
// Showcase: until the shadow layer is revealed, render fully lit.
int sc_mask = ShowcaseMask(px.x);
if (sc_mask >= 0 && (sc_mask & 8) == 0) {
return 1.0;
}
float2 uc_far = float2(dot(nsm_x.xyz, wp) + nsm_x.w, float2 uc_far = float2(dot(nsm_x.xyz, wp) + nsm_x.w,
dot(nsm_y.xyz, wp) + nsm_y.w); dot(nsm_y.xyz, wp) + nsm_y.w);
// Screen-pixel footprint in far-tile uc, BEFORE any divergent return // Screen-pixel footprint in far-tile uc, BEFORE any divergent return

File diff suppressed because it is too large Load diff

View file

@ -38,7 +38,9 @@ cbuffer C : register(b0) {
float4 p0; // x = march steps, y = thickness (view units), float4 p0; // x = march steps, y = thickness (view units),
// z = composite intensity, w = debug mode // z = composite intensity, w = debug mode
float4 p1; // x = max ray distance (view units) float4 p1; // x = max ray distance (view units)
float4 p2; // spare float4 p2; // graphics build-up showcase: x = split position in output
// pixels, y/z = SSR visibility (0/1) left/right of the
// split (both 1 when the showcase is off)
}; };
// Per-pass t0/t1/t2 roles (each entry uses one of the overlapping // Per-pass t0/t1/t2 roles (each entry uses one of the overlapping
// declarations; the rest drop out at compile): // declarations; the rest drop out at compile):
@ -283,6 +285,11 @@ float4 ps_march(VSOut i) : SV_Target {
} }
float4 ps_composite(VSOut i) : SV_Target { float4 ps_composite(VSOut i) : SV_Target {
// Showcase gate: alpha 0 leaves the scene plane untouched on a side
// whose build-up stage has not revealed reflections yet.
if ((i.pos.x < p2.x ? p2.y : p2.z) < 0.5f) {
return float4(0.0f, 0.0f, 0.0f, 0.0f);
}
float4 g = tex1c.SampleLevel(smp_point, i.uv, 0); float4 g = tex1c.SampleLevel(smp_point, i.uv, 0);
float wrefl = abs(g.a); float wrefl = abs(g.a);
float dbg = p0.w; float dbg = p0.w;

View file

@ -81,6 +81,8 @@ extern const rex::PPCImageInfo eawebkit_PPCImageConfig;
// lift via config because they overlap with auto-discovered parent functions. // lift via config because they overlap with auto-discovered parent functions.
extern "C" REX_FUNC(__restgprlr_19); extern "C" REX_FUNC(__restgprlr_19);
// Defined in skate3_native_scene.cpp; toggled by the showcase hotkey below.
REXCVAR_DECLARE(bool, skate3_native_render_scene_showcase);
// Defined in skate3_native_scene.cpp; the freecam hotkey below toggles it, // Defined in skate3_native_scene.cpp; the freecam hotkey below toggles it,
// and while it captures input the guest input system is gated off. // and while it captures input the guest input system is gated off.
REXCVAR_DECLARE(bool, skate3_native_render_scene_freecam); REXCVAR_DECLARE(bool, skate3_native_render_scene_freecam);
@ -762,6 +764,12 @@ void Skate3BaseApp::OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) {
"Native render debug menu", [this] { "Native render debug menu", [this] {
ToggleNativeDebug(); ToggleNativeDebug();
}); });
rex::ui::RegisterBind("bind_skate3_showcase", "Home",
"Graphics build-up showcase", [] {
REXCVAR_SET(
skate3_native_render_scene_showcase,
!REXCVAR_GET(skate3_native_render_scene_showcase));
});
rex::ui::RegisterBind("bind_skate3_freecam", "End", rex::ui::RegisterBind("bind_skate3_freecam", "End",
"Drone camera (free fly)", [] { "Drone camera (free fly)", [] {
REXCVAR_SET( REXCVAR_SET(

View file

@ -2,6 +2,8 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <string>
#include <vector>
#include <imgui.h> #include <imgui.h>
@ -84,10 +86,15 @@ REXCVAR_DECLARE(double, skate3_native_render_scene_synthetic_pan_rate);
REXCVAR_DECLARE(double, skate3_native_render_scene_synthetic_pan_amp); REXCVAR_DECLARE(double, skate3_native_render_scene_synthetic_pan_amp);
// SDK: emulated-draw suppression while the native output is active. // SDK: emulated-draw suppression while the native output is active.
REXCVAR_DECLARE(bool, native_render_suppress_emulated_draws); REXCVAR_DECLARE(bool, native_render_suppress_emulated_draws);
REXCVAR_DECLARE(bool, skate3_native_render_scene_showcase);
REXCVAR_DECLARE(double, skate3_native_render_scene_showcase_hold);
REXCVAR_DECLARE(double, skate3_native_render_scene_showcase_wipe);
REXCVAR_DECLARE(std::string, skate3_native_render_scene_showcase_order);
REXCVAR_DECLARE(bool, skate3_native_render_scene_freecam); REXCVAR_DECLARE(bool, skate3_native_render_scene_freecam);
REXCVAR_DECLARE(double, skate3_native_render_scene_freecam_speed); REXCVAR_DECLARE(double, skate3_native_render_scene_freecam_speed);
REXCVAR_DECLARE(double, skate3_native_render_scene_freecam_look_speed); REXCVAR_DECLARE(double, skate3_native_render_scene_freecam_look_speed);
REXCVAR_DECLARE(bool, skate3_native_render_scene_freecam_capture_input); REXCVAR_DECLARE(bool, skate3_native_render_scene_freecam_capture_input);
REXCVAR_DECLARE(bool, skate3_native_render_scene_ssr);
namespace skate3 { namespace skate3 {
namespace { namespace {
@ -115,6 +122,118 @@ double CvarSlider(const char* label, double value, float lo, float hi,
return value; return value;
} }
// ---- Showcase setup window ------------------------------------------------
// Editor for skate3_native_render_scene_showcase_order: one row per build-up
// layer, reorderable, each optionally joined to the previous row's wipe. The
// cvar string is the single source of truth ("tok,tok+tok,-tok": ',' starts
// a new wipe, '+' joins, '-' disables in place); the rows parse from it
// every frame and any edit writes it straight back, so the sequencer, this
// window and hand edits in config all stay consistent.
bool s_showcase_setup_open = false;
struct ShowcaseRow {
const skate3::native_scene::ShowcaseLayer* layer;
bool enabled;
bool joined;
};
std::vector<ShowcaseRow> ParseShowcaseRows(const std::string& order) {
using skate3::native_scene::kShowcaseLayers;
std::vector<ShowcaseRow> rows;
std::string tok;
bool cur_joined = false; // the separator before the accumulating token
const auto flush = [&]() {
while (!tok.empty() && (tok.front() == ' ' || tok.front() == '\t')) {
tok.erase(tok.begin());
}
while (!tok.empty() && (tok.back() == ' ' || tok.back() == '\t')) {
tok.pop_back();
}
bool enabled = true;
if (!tok.empty() && tok[0] == '-') {
enabled = false;
tok.erase(tok.begin());
}
for (char& ch : tok) {
if (ch >= 'A' && ch <= 'Z') {
ch = char(ch + 32);
}
}
for (const auto& layer : kShowcaseLayers) {
if (tok != layer.token) {
continue;
}
bool present = false;
for (const auto& row : rows) {
present = present || row.layer == &layer;
}
if (!present) {
rows.push_back({&layer, enabled, !rows.empty() && cur_joined});
}
break;
}
tok.clear();
};
for (char ch : order) {
if (ch == ',' || ch == '+') {
flush();
cur_joined = ch == '+';
} else {
tok += ch;
}
}
flush();
// Layers missing from the string append disabled, so they can always be
// re-enabled from the window.
for (const auto& layer : kShowcaseLayers) {
bool present = false;
for (const auto& row : rows) {
present = present || row.layer == &layer;
}
if (!present) {
rows.push_back({&layer, false, false});
}
}
return rows;
}
std::string BuildShowcaseOrder(const std::vector<ShowcaseRow>& rows) {
std::string out;
for (size_t k = 0; k < rows.size(); ++k) {
if (k > 0) {
out += rows[k].joined ? '+' : ',';
}
if (!rows[k].enabled) {
out += '-';
}
out += rows[k].layer->token;
}
return out;
}
// Mirrors the sequencer's availability rules (TickShowcase): the material
// looks need nothing; shadow/post layers need their feature (post gates
// live in the HDR tonemap).
bool ShowcaseLayerAvailable(uint32_t bit) {
const bool hdr_on = REXCVAR_GET(skate3_native_render_scene_hdr);
switch (bit) {
case 8u:
return REXCVAR_GET(skate3_native_render_scene_shadows);
case 16u:
return hdr_on && REXCVAR_GET(skate3_native_render_scene_ssao);
case 32u:
return hdr_on && REXCVAR_GET(skate3_native_render_scene_ssr);
case 64u:
return hdr_on && (REXCVAR_GET(skate3_native_render_scene_shafts) ||
REXCVAR_GET(skate3_native_render_scene_haze));
case 128u:
return hdr_on && REXCVAR_GET(skate3_native_render_scene_bloom);
default:
return true;
}
}
// Drone-camera controls (same cvars everywhere the section is embedded; // Drone-camera controls (same cvars everywhere the section is embedded;
// edits apply live). // edits apply live).
void DrawFreecamControls() { void DrawFreecamControls() {
@ -148,6 +267,130 @@ void DrawFreecamControls() {
"Z/X zoom, Shift fast, Ctrl slow."); "Z/X zoom, Shift fast, Ctrl slow.");
} }
void DrawShowcaseSetupWindow(bool* p_open) {
using skate3::native_scene::kShowcaseLayers;
ImGui::SetNextWindowSize(ImVec2(430.0f, 0.0f), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Showcase Setup", p_open, ImGuiWindowFlags_NoCollapse)) {
ImGui::End();
return;
}
const bool running = REXCVAR_GET(skate3_native_render_scene_showcase);
if (ImGui::Button(running ? "Cancel showcase" : "Start showcase (Home)",
ImVec2(-1.0f, 0.0f))) {
REXCVAR_SET(skate3_native_render_scene_showcase, !running);
}
REXCVAR_SET(skate3_native_render_scene_showcase_hold,
CvarSlider("stage hold (s)",
REXCVAR_GET(skate3_native_render_scene_showcase_hold),
0.0f, 10.0f, "%.1f",
"Seconds each layer holds fullscreen between wipes"));
REXCVAR_SET(skate3_native_render_scene_showcase_wipe,
CvarSlider("wipe duration (s)",
REXCVAR_GET(skate3_native_render_scene_showcase_wipe),
0.5f, 10.0f, "%.1f",
"Seconds each split wipe takes to cross the screen"));
ImGui::SeparatorText("Layer order");
ImGui::TextDisabled(
"Every run starts from clay geometry. \"join\" reveals a layer\n"
"inside the previous layer's wipe instead of its own.");
std::vector<ShowcaseRow> rows =
ParseShowcaseRows(REXCVAR_GET(skate3_native_render_scene_showcase_order));
bool changed = false;
for (int k = 0; k < int(rows.size()); ++k) {
ImGui::PushID(k);
bool en = rows[k].enabled;
if (ImGui::Checkbox("##on", &en)) {
rows[k].enabled = en;
changed = true;
}
ImGui::SameLine();
ImGui::BeginDisabled(k == 0);
if (ImGui::ArrowButton("up", ImGuiDir_Up) && k > 0) {
std::swap(rows[k - 1], rows[k]);
changed = true;
}
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::BeginDisabled(k + 1 == int(rows.size()));
if (ImGui::ArrowButton("down", ImGuiDir_Down) && k + 1 < int(rows.size())) {
std::swap(rows[k], rows[k + 1]);
changed = true;
}
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::BeginDisabled(k == 0);
bool joined = k > 0 && rows[k].joined;
if (ImGui::Checkbox("join", &joined)) {
rows[k].joined = joined;
changed = true;
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ShowcaseLayerAvailable(rows[k].layer->bit)) {
ImGui::TextUnformatted(rows[k].layer->label);
} else {
ImGui::TextDisabled("%s (feature off)", rows[k].layer->label);
}
ImGui::PopID();
}
if (changed) {
if (!rows.empty()) {
rows[0].joined = false;
}
REXCVAR_SET(skate3_native_render_scene_showcase_order,
BuildShowcaseOrder(rows));
}
if (ImGui::Button("Reset to default order")) {
REXCVAR_SET(skate3_native_render_scene_showcase_order,
std::string(skate3::native_scene::kShowcaseOrderDefault));
}
// Live preview of the resulting run, mirrors the sequencer's step
// builder (cumulative layer masks; materials subsumes albedo/lighting;
// a final full-render step covers whatever the list leaves out).
ImGui::SeparatorText("Run preview");
uint32_t avail_mask = 0;
for (const auto& layer : kShowcaseLayers) {
if (ShowcaseLayerAvailable(layer.bit)) {
avail_mask |= layer.bit;
}
}
int step_no = 1;
ImGui::Text("%d. clay geometry", step_no++);
uint32_t cum = 0;
size_t k = 0;
while (k < rows.size()) {
uint32_t add = 0;
std::string label;
size_t j = k;
do {
const ShowcaseRow& row = rows[j];
if (row.enabled && (avail_mask & row.layer->bit) != 0 &&
(cum & row.layer->bit) == 0 && (add & row.layer->bit) == 0) {
add |= row.layer->bit;
if (!label.empty()) {
label += " & ";
}
label += row.layer->label;
}
++j;
} while (j < rows.size() && rows[j].joined);
if (add != 0) {
cum |= add;
ImGui::Text("%d. + %s", step_no++, label.c_str());
}
k = j;
}
const bool covered = (cum & 4u) != 0 && ((avail_mask & ~cum) & 0xF8u) == 0u;
if (!covered) {
ImGui::Text("%d. full render", step_no++);
}
DrawFreecamControls();
ImGui::End();
}
} // namespace } // namespace
void NativeDebugDialog::Show() { void NativeDebugDialog::Show() {
@ -240,6 +483,25 @@ void NativeDebugDialog::OnDraw(ImGuiIO& io) {
"World env materials cull FRONT like the game's material " "World env materials cull FRONT like the game's material "
"XMLs; off = legacy cull-none (shows interior faces)")); "XMLs; off = legacy cull-none (shows interior faces)"));
ImGui::SeparatorText("Graphics showcase");
{
const bool running = REXCVAR_GET(skate3_native_render_scene_showcase);
if (ImGui::Button(running ? "Cancel showcase" : "Start showcase (Home)")) {
REXCVAR_SET(skate3_native_render_scene_showcase, !running);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Strip the frame to clay geometry, then rebuild it layer by\n"
"layer, each layer revealed by a live vertical split wiping\n"
"across the screen. Layer order, grouping and pacing are edited\n"
"in the setup window.");
}
ImGui::SameLine();
if (ImGui::Button("Setup...")) {
s_showcase_setup_open = true;
}
}
DrawFreecamControls(); DrawFreecamControls();
ImGui::SeparatorText("HDR post effects (live)"); ImGui::SeparatorText("HDR post effects (live)");
@ -674,6 +936,9 @@ void NativeDebugDialog::OnDraw(ImGuiIO& io) {
ImGui::TextDisabled("MSAA + shadow tile size are restart-only (skate3.toml)."); ImGui::TextDisabled("MSAA + shadow tile size are restart-only (skate3.toml).");
ImGui::End(); ImGui::End();
if (s_showcase_setup_open) {
DrawShowcaseSetupWindow(&s_showcase_setup_open);
}
if (!open) { if (!open) {
Hide(); Hide();
} }

View file

@ -292,6 +292,38 @@ REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_haze_density, 0.005,
"200 units).") "200 units).")
.range(0.0, 0.05) .range(0.0, 0.05)
.lifecycle(rex::cvar::Lifecycle::kHotReload); .lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_showcase, false, "Skate 3",
"Run the graphics build-up showcase: the scene is stripped to "
"flat clay geometry, then rebuilt layer by layer, each layer "
"revealed by a vertical split wiping across the screen with "
"both sides rendered live. Layer order and grouping come from "
"skate3_native_render_scene_showcase_order (edited in the F12 "
"showcase setup window). Clears itself when the sequence "
"finishes; set to false mid-run to cancel. Also on the Home "
"key by default (bind_skate3_showcase). Layers whose feature "
"is disabled (or unavailable without the HDR intermediate) "
"are skipped.")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_STRING(
skate3_native_render_scene_showcase_order,
skate3::native_scene::kShowcaseOrderDefault, "Skate 3",
"Showcase layer order: comma-separated reveal steps, '+' joins layers "
"into one wipe, a '-' prefix disables a layer while keeping its "
"position. Tokens: albedo, lighting, materials, shadows, ao, ssr, vol, "
"bloom (the run always starts from clay geometry, and a final "
"full-render step is appended when the list leaves layers unrevealed). "
"Example: \"albedo,lighting,materials,shadows+ao,-ssr,vol+bloom\".")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_showcase_hold, 2.5, "Skate 3",
"Showcase: seconds each build-up stage holds fullscreen "
"between wipes.")
.range(0.0, 30.0)
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_showcase_wipe, 3.0, "Skate 3",
"Showcase: seconds each split wipe takes to cross the "
"screen.")
.range(0.2, 30.0)
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_freecam, false, "Skate 3", REXCVAR_DEFINE_BOOL(skate3_native_render_scene_freecam, false, "Skate 3",
"Detach the render camera from the game (drone / free-fly " "Detach the render camera from the game (drone / free-fly "
"cam): WASD fly, E/Space up, Q/C down, arrow keys or " "cam): WASD fly, E/Space up, Q/C down, arrow keys or "

View file

@ -435,6 +435,34 @@ struct SubmitRecord {
uint32_t c; uint32_t c;
}; };
// Graphics build-up showcase layer registry: the order-string tokens
// (cvar skate3_native_render_scene_showcase_order), the per-layer shader
// mask bits (contract in scene.hlsl ShowcaseMask: the split rows carry
// 256 + the cumulative mask, 0 = showcase off) and the display labels.
// Shared by the sequencer (skate3_native_scene_gpu.cpp) and the F12 setup
// window (skate3_native_debug_dialog.cpp). The material bits are
// progressive looks (materials subsumes lighting subsumes albedo); the
// rest are independent and can reveal in any order or grouping.
struct ShowcaseLayer {
const char* token;
uint32_t bit;
const char* label;
};
inline constexpr ShowcaseLayer kShowcaseLayers[] = {
{"albedo", 1u, "albedo textures"},
{"lighting", 2u, "baked lighting"},
{"materials", 4u, "materials & surface detail"},
{"shadows", 8u, "dynamic shadows"},
{"ao", 16u, "ambient occlusion"},
{"ssr", 32u, "reflections"},
{"vol", 64u, "volumetrics"},
{"bloom", 128u, "bloom"},
};
inline constexpr size_t kShowcaseLayerCount =
sizeof(kShowcaseLayers) / sizeof(kShowcaseLayers[0]);
inline constexpr const char* kShowcaseOrderDefault =
"albedo,lighting,materials,shadows+ao,ssr,vol+bloom";
bool Enabled(); bool Enabled();
// Host settings-overlay backdrop: while the SDK settings menu is open, run // Host settings-overlay backdrop: while the SDK settings menu is open, run

View file

@ -106,6 +106,9 @@ REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_static_radius);
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_shadow_static_size); REXCVAR_DECLARE(int32_t, skate3_native_render_scene_shadow_static_size);
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_static_strength); REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_static_strength);
REXCVAR_DECLARE(bool, skate3_native_render_scene_shadows); REXCVAR_DECLARE(bool, skate3_native_render_scene_shadows);
REXCVAR_DECLARE(bool, skate3_native_render_scene_shafts);
REXCVAR_DECLARE(bool, skate3_native_render_scene_haze);
REXCVAR_DECLARE(bool, skate3_native_render_scene_showcase);
REXCVAR_DECLARE(bool, skate3_native_render_scene_sort_opaque); REXCVAR_DECLARE(bool, skate3_native_render_scene_sort_opaque);
REXCVAR_DECLARE(bool, skate3_native_render_scene_splines); REXCVAR_DECLARE(bool, skate3_native_render_scene_splines);
REXCVAR_DECLARE(bool, skate3_native_render_scene_ssao); REXCVAR_DECLARE(bool, skate3_native_render_scene_ssao);
@ -127,6 +130,9 @@ REXCVAR_DECLARE(double, skate3_native_render_scene_refl_lod);
REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_intensity); REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_intensity);
REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_luma_protect); REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_luma_protect);
REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_radius); REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_radius);
REXCVAR_DECLARE(double, skate3_native_render_scene_showcase_hold);
REXCVAR_DECLARE(double, skate3_native_render_scene_showcase_wipe);
REXCVAR_DECLARE(std::string, skate3_native_render_scene_showcase_order);
REXCVAR_DECLARE(double, skate3_native_render_scene_ssr_intensity); REXCVAR_DECLARE(double, skate3_native_render_scene_ssr_intensity);
REXCVAR_DECLARE(double, skate3_native_render_scene_ssr_thickness); REXCVAR_DECLARE(double, skate3_native_render_scene_ssr_thickness);
REXCVAR_DECLARE(double, skate3_native_render_scene_world_v2_tan_sign); REXCVAR_DECLARE(double, skate3_native_render_scene_world_v2_tan_sign);
@ -6523,6 +6529,185 @@ void LogFrameStats(const FrameScene& scene, uint64_t frames, uint32_t drawn,
} }
} }
// ---- Graphics build-up showcase -------------------------------------------
// Sequencer for the layer-by-layer showcase (cvar
// skate3_native_render_scene_showcase / the bind_skate3_showcase hotkey):
// one wipe strips the frame to clay geometry, then each following wipe
// reveals the next visual layer, a vertical split sweeping left-to-right
// with BOTH sides rendered live (per-pixel stage selection in the shaders,
// no frozen frames). The split state rides the per-frame b1 rows
// (scene.hlsl sh_v2.yzw) and g_r.showcase_rows (the SSR composite reads it
// from root constants); every consumer treats an all-zero row as "showcase
// off". Runs on the render thread once per frame at b1 staging. The layer
// mask encoding matches the shader contract in scene.hlsl ShowcaseMask;
// the reveal order and grouping come from the user-editable
// skate3_native_render_scene_showcase_order cvar (kShowcaseLayers tokens).
static void TickShowcase(uint32_t output_width, bool hdr_on) {
// One reveal step: the b1 row encoding (256 + the cumulative layer mask;
// 0 = the full render) and its log label.
struct Step {
float value;
std::string label;
};
struct State {
bool active = false;
std::vector<Step> steps;
int logged = -1;
double wipe_s = 3.0;
double hold_s = 2.5;
PerfClock::time_point t0;
};
static State s;
float* rows = g_r.showcase_rows;
rows[0] = rows[1] = rows[2] = 0.0f;
const bool want = REXCVAR_GET(skate3_native_render_scene_showcase);
if (!s.active) {
if (!want) {
return;
}
// Layer availability: the material looks need nothing; the shadow and
// post layers join only when their feature is enabled (the post gates
// live in the HDR tonemap, so they need the HDR chain).
const auto layer_available = [&](uint32_t bit) -> bool {
switch (bit) {
case 8u:
return REXCVAR_GET(skate3_native_render_scene_shadows);
case 16u:
return hdr_on && REXCVAR_GET(skate3_native_render_scene_ssao);
case 32u:
return hdr_on && REXCVAR_GET(skate3_native_render_scene_ssr);
case 64u:
return hdr_on && (REXCVAR_GET(skate3_native_render_scene_shafts) ||
REXCVAR_GET(skate3_native_render_scene_haze));
case 128u:
return hdr_on && REXCVAR_GET(skate3_native_render_scene_bloom);
default:
return true;
}
};
uint32_t avail_mask = 0;
for (const auto& layer : kShowcaseLayers) {
if (layer_available(layer.bit)) {
avail_mask |= layer.bit;
}
}
// Build the reveal steps from the user-ordered layer list (syntax in
// the skate3_native_render_scene_showcase_order cvar description):
// comma-separated steps, '+' joins layers into one wipe, '-' prefix =
// disabled. Unknown, unavailable and duplicate tokens drop out; steps
// left empty are skipped. The run always starts from clay.
s.steps.clear();
s.steps.push_back({256.0f, "clay geometry"});
uint32_t cum = 0;
const std::string order =
REXCVAR_GET(skate3_native_render_scene_showcase_order);
size_t pos = 0;
while (pos <= order.size()) {
size_t end = order.find(',', pos);
if (end == std::string::npos) {
end = order.size();
}
const std::string group = order.substr(pos, end - pos);
pos = end + 1;
uint32_t add = 0;
std::string label;
size_t g = 0;
while (g <= group.size()) {
size_t ge = group.find('+', g);
if (ge == std::string::npos) {
ge = group.size();
}
std::string tok = group.substr(g, ge - g);
g = ge + 1;
while (!tok.empty() && (tok.front() == ' ' || tok.front() == '\t')) {
tok.erase(tok.begin());
}
while (!tok.empty() && (tok.back() == ' ' || tok.back() == '\t')) {
tok.pop_back();
}
for (char& ch : tok) {
if (ch >= 'A' && ch <= 'Z') {
ch = char(ch + 32);
}
}
if (tok.empty() || tok[0] == '-') {
continue; // disabled layer (kept in the string for its position)
}
for (const auto& layer : kShowcaseLayers) {
if (tok == layer.token) {
if ((avail_mask & layer.bit) != 0 && (cum & layer.bit) == 0 &&
(add & layer.bit) == 0) {
add |= layer.bit;
if (!label.empty()) {
label += " & ";
}
label += layer.label;
}
break;
}
}
}
if (add != 0) {
cum |= add;
s.steps.push_back({float(256u + cum), std::move(label)});
}
}
// A final full-render reveal when the list leaves layers unrevealed
// (the materials look subsumes the albedo/lighting bits, so those two
// stop counting once materials is in).
const bool covered =
(cum & 4u) != 0 && ((avail_mask & ~cum) & 0xF8u) == 0u;
if (!covered) {
s.steps.push_back({0.0f, "full render"});
}
// Durations snapshot at start so a live cvar edit cannot jump the
// elapsed-time -> step mapping mid-run.
s.wipe_s = std::max(0.2, REXCVAR_GET(skate3_native_render_scene_showcase_wipe));
s.hold_s = std::max(0.0, REXCVAR_GET(skate3_native_render_scene_showcase_hold));
s.t0 = PerfClock::now();
s.logged = -1;
s.active = true;
REXLOG_INFO(
"native-scene: showcase started ({} steps, wipe {:.1f}s, hold "
"{:.1f}s, hdr={}, order=\"{}\")",
s.steps.size(), s.wipe_s, s.hold_s, hdr_on ? 1 : 0, order);
}
if (!want) {
s.active = false;
REXLOG_INFO("native-scene: showcase cancelled");
return;
}
const double elapsed =
std::chrono::duration<double>(PerfClock::now() - s.t0).count();
const double per_step = s.wipe_s + s.hold_s;
const int idx = int(elapsed / per_step);
if (idx >= int(s.steps.size())) {
s.active = false;
REXCVAR_SET(skate3_native_render_scene_showcase, false);
REXLOG_INFO("native-scene: showcase finished");
return;
}
const float cur = s.steps[idx].value;
if (idx != s.logged) {
REXLOG_INFO("native-scene: showcase step {}/{} - {}", idx + 1,
s.steps.size(), s.steps[idx].label);
s.logged = idx;
}
const float prev = idx == 0 ? 0.0f : s.steps[idx - 1].value;
const double phase = elapsed - double(idx) * per_step;
if (phase < s.wipe_s) {
float f = float(phase / s.wipe_s);
f = f * f * (3.0f - 2.0f * f); // ease the sweep in and out
rows[0] = cur;
rows[1] = prev;
// Overshoot slightly so the divider line exits the screen edge cleanly.
rows[2] = f * float(output_width) * 1.02f;
} else {
rows[0] = rows[1] = cur;
rows[2] = 0.0f;
}
}
bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_data*/) { bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_data*/) {
if (!SceneEnabled() || if (!SceneEnabled() ||
(context.backend != NativeGuestOutputBackend::kD3D12 && (context.backend != NativeGuestOutputBackend::kD3D12 &&
@ -7009,9 +7194,14 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
std::memcpy(cb + 40, scene.fog_color, 4 * sizeof(float)); std::memcpy(cb + 40, scene.fog_color, 4 * sizeof(float));
} }
// World-shading v2 row (sh_v2, cb[60..63]): x = stored-tangent // World-shading v2 row (sh_v2, cb[60..63]): x = stored-tangent
// polarity. // polarity, yzw = the build-up showcase split state (stage left/right
// of the split + its position in output pixels; zeros = showcase off).
cb[60] = cb[60] =
float(REXCVAR_GET(skate3_native_render_scene_world_v2_tan_sign)); float(REXCVAR_GET(skate3_native_render_scene_world_v2_tan_sign));
TickShowcase(context.guest_output_width, hdr_on);
cb[61] = g_r.showcase_rows[0];
cb[62] = g_r.showcase_rows[1];
cb[63] = g_r.showcase_rows[2];
// dynamicobject.fx frame rows (dyn_sun/dyn_amb/dyn_misc at cb[44..55]). // dynamicobject.fx frame rows (dyn_sun/dyn_amb/dyn_misc at cb[44..55]).
if (scene.dynobj_valid) { if (scene.dynobj_valid) {
cb[44] = scene.dynobj_rows[0]; // sun dir (PS c9) cb[44] = scene.dynobj_rows[0]; // sun dir (PS c9)

View file

@ -726,6 +726,11 @@ struct RendererState {
// staged by ApplyVolumetricPass; zeros disable every term. // staged by ApplyVolumetricPass; zeros disable every term.
bool vol_tonemap_valid = false; bool vol_tonemap_valid = false;
float vol_rows[16] = {}; float vol_rows[16] = {};
// Graphics build-up showcase split state for this frame, computed by the
// sequencer at b1 staging and mirrored here for the SSR composite:
// {stage left of the split, stage right, split position in output px}.
// All zeros = showcase off (stage 0 = the full render in every consumer).
float showcase_rows[3] = {};
std::unordered_map<uint32_t, MeshBuffers> meshes; std::unordered_map<uint32_t, MeshBuffers> meshes;
// (The old D3D12 bookkeeping, the retired-resource vector, the CPU SRV // (The old D3D12 bookkeeping, the retired-resource vector, the CPU SRV
// staging heap and its slot allocator/recycling lists, is gone: resource // staging heap and its slot allocator/recycling lists, is gone: resource

View file

@ -781,6 +781,17 @@ bool ApplySsrPass(const NativeGuestOutputRenderContext& context,
c[22] = float(REXCVAR_GET(skate3_native_render_scene_ssr_intensity)); c[22] = float(REXCVAR_GET(skate3_native_render_scene_ssr_intensity));
c[23] = float(REXCVAR_GET(skate3_native_render_scene_ssr_debug)); c[23] = float(REXCVAR_GET(skate3_native_render_scene_ssr_debug));
c[24] = 250.0f; // max ray distance (view units; the screen clip governs) c[24] = 250.0f; // max ray distance (view units; the screen clip governs)
// Build-up showcase gate (p2, see ssr.hlsl ps_composite): the composite
// side whose layer mask has not revealed reflections outputs alpha 0.
// Rows carry 256 + mask (0 = showcase off); SSR is mask bit 32.
const auto ssr_visible = [](float row) {
return (row < 255.5f || ((uint32_t(row + 0.5f) - 256u) & 32u) != 0u)
? 1.0f
: 0.0f;
};
c[28] = g_r.showcase_rows[2];
c[29] = ssr_visible(g_r.showcase_rows[0]);
c[30] = ssr_visible(g_r.showcase_rows[1]);
cmd->SetBindingLayout(g_r.ssr_layout); cmd->SetBindingLayout(g_r.ssr_layout);
cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList); cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList);