render: screen-space reflections (experimental, off by default)
This commit is contained in:
parent
3e09f33daf
commit
80499b9f06
6 changed files with 2000 additions and 0 deletions
|
|
@ -282,6 +282,7 @@ set(SKATE3_NATIVE_SHADER_SOURCES
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/photo_fx.hlsl
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/ssao.hlsl
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/hdr.hlsl
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/ssr.hlsl
|
||||
)
|
||||
set(SKATE3_NATIVE_SHADER_HEADER
|
||||
${CMAKE_CURRENT_BINARY_DIR}/include/skate3_native_shaders.h)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ set(SHADER_VARS
|
|||
kPhotoFxShaderSource
|
||||
kSsaoShaderSource
|
||||
kHdrShaderSource
|
||||
kSsrShaderSource
|
||||
)
|
||||
set(SHADER_FILES
|
||||
scene.hlsl
|
||||
|
|
@ -28,6 +29,7 @@ set(SHADER_FILES
|
|||
photo_fx.hlsl
|
||||
ssao.hlsl
|
||||
hdr.hlsl
|
||||
ssr.hlsl
|
||||
)
|
||||
|
||||
set(content "// Generated by cmake/EmbedShaders.cmake from src/native/shaders/*.hlsl.\n")
|
||||
|
|
|
|||
|
|
@ -1102,6 +1102,97 @@ float4 ps_main(VSOut i) : SV_Target {
|
|||
}
|
||||
return float4(PassGamma(lit), 1.0);
|
||||
}
|
||||
// ---- SSR reflection G-buffer (consumed by ssr.hlsl ps_march) --------------
|
||||
// Rendered after the main pass from the frame's reflective items only (env
|
||||
// families 5/6/13 + water), half res, single sample, NO depth buffer:
|
||||
// RG = octahedral WORLD-space normal, B = linear view depth (the clip w),
|
||||
// A = reflectivity weight, negated for surfaces that never write scene
|
||||
// depth (water / blended fam-13 glass), which selects the march's in-front
|
||||
// visibility rule instead of the exact depth match. The caller re-stages
|
||||
// each item's main-pass root constants and t3/t4/t5 textures, so the normal
|
||||
// composition below is the material branch's own math.
|
||||
float2 OctEncode(float3 n) {
|
||||
n /= abs(n.x) + abs(n.y) + abs(n.z);
|
||||
if (n.z < 0.0) {
|
||||
n.xy = float2((1.0 - abs(n.y)) * (n.x >= 0.0 ? 1.0 : -1.0),
|
||||
(1.0 - abs(n.x)) * (n.y >= 0.0 ? 1.0 : -1.0));
|
||||
}
|
||||
return n.xy;
|
||||
}
|
||||
float4 ps_refl_gbuf(VSOut i) : SV_Target {
|
||||
float3 wn;
|
||||
float w;
|
||||
float trans;
|
||||
// Env families carry cam_pos.w = -fam; water rides the legacy branch with
|
||||
// cam_pos.w = 0 (misc.x is the packed normal-trim on fams 5/6 here, so it
|
||||
// cannot discriminate).
|
||||
if (cam_pos.w < -0.5) {
|
||||
// Fams 5/6/13: the reflective branch's own normal + reflection mask.
|
||||
// No alpha test; masked reflective draws skip the clip in ps_main too
|
||||
// (overlay.w >= 3).
|
||||
float fam = -cam_pos.w;
|
||||
float4 masks = decal_art.Sample(smp, i.uv);
|
||||
// Blend weight, not the material's energy term: the raw refmask
|
||||
// (~0.2-0.5 on glass) is authored to scale the cube ADDEND; using it
|
||||
// directly as the SSR lerp weight leaves the cube fallback dominating
|
||||
// and the reflections imperceptible. Boost so real glass approaches
|
||||
// full replacement while zero-mask texels stay out.
|
||||
w = saturate(masks.z * 2.5);
|
||||
trans = fam > 12.5 ? 1.0 : 0.0;
|
||||
wn = dot(i.nrm, i.nrm) > 0.01
|
||||
? normalize(i.nrm)
|
||||
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
|
||||
if (overlay.w > 3.5) {
|
||||
// Normal-mapped per-panel tilts (fams 5/6): the same composition as
|
||||
// the material branch (t5 map + the constant detail-fold trim packed
|
||||
// in misc.x; analytic world-up axes carrying the screen-frame signs).
|
||||
float3 tt, bb;
|
||||
ScreenTangentFrame(wn, i.rpos, i.uv, tt, bb);
|
||||
float3 bb2 = float3(0.0, 1.0, 0.0) - wn * wn.y;
|
||||
float lb2 = length(bb2);
|
||||
if (lb2 > 0.05) {
|
||||
bb2 /= lb2;
|
||||
float3 tt2 = cross(bb2, wn);
|
||||
tt = tt2 * (dot(tt2, tt) >= 0.0 ? 1.0 : -1.0);
|
||||
bb = bb2 * (dot(bb2, bb) >= 0.0 ? 1.0 : -1.0);
|
||||
}
|
||||
float trim_yi = floor(misc.x / 1000.0);
|
||||
float2 trim = float2(misc.x - trim_yi * 1000.0 - 500.0,
|
||||
trim_yi - 500.0) * 0.001;
|
||||
float3 nmv = normal_map.Sample(smp, i.uv).rgb;
|
||||
float3 nt = float3(nmv.xy * 2.0 - 1.0 + trim, nmv.z * 2.0 - 1.0);
|
||||
wn = normalize(nt.x * tt + nt.y * bb + wn * max(nt.z, 0.05));
|
||||
}
|
||||
} else {
|
||||
// Water: the water branch's ripple-perturbed up normal; reflectivity =
|
||||
// the branch's own fresnel-scaled reflection weight (0.55 + 0.45*fres).
|
||||
float t = overlay.x;
|
||||
float2 rip;
|
||||
if (overlay.z > 0.0) {
|
||||
float2 wuv = i.uv * 6.0;
|
||||
float3 n1 = macro.Sample(smp, wuv + t * float2(0.11, 0.06)).rgb;
|
||||
float3 n2 = macro.Sample(smp, wuv * 1.71 - t * float2(0.07, 0.13)).rgb;
|
||||
rip = (n1.xy + n2.xy) - 1.0;
|
||||
} else {
|
||||
float3 wp = i.rpos + cam_pos.xyz;
|
||||
rip = 0.35 * float2(sin(wp.x * 9.7 + wp.z * 6.1 + t * 2.3) +
|
||||
0.6 * sin(wp.x * 17.3 - wp.z * 11.9 + t * 3.4),
|
||||
cos(wp.x * 7.1 - wp.z * 13.7 + t * 2.7) +
|
||||
0.6 * cos(wp.x * 12.9 + wp.z * 18.3 + t * 3.1));
|
||||
}
|
||||
wn = normalize(float3(rip.x * 0.4, 2.0, rip.y * 0.4));
|
||||
float3 vd = -normalize(i.rpos);
|
||||
float fres = pow(1.0 - saturate(dot(vd, wn)), 3.0);
|
||||
w = 0.55 + 0.45 * fres;
|
||||
trans = 1.0;
|
||||
}
|
||||
// B = the view depth in meters. SV_Position.w here delivers clip-space w
|
||||
// directly, which equals view Z under the row-vector projection (m23 = 1)
|
||||
// - verified empirically: marches from ViewPos(uv, pos.w) produce
|
||||
// geometrically coherent reflections, and inverting this value collapses
|
||||
// every downstream depth test (all rays die at the visibility gate).
|
||||
return float4(OctEncode(wn), i.pos.w, w * (trans > 0.5 ? -1.0 : 1.0));
|
||||
}
|
||||
// Shadow caster pass: vs_main runs with mvp = (world *) lightVP built from
|
||||
// the captured receiver rows, so SV_Position.z IS the light-space depth
|
||||
// (the height-ramp row; viewport z range 0..1, DepthClip off so casters
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
333
src/native/shaders/ssr.hlsl
Normal file
333
src/native/shaders/ssr.hlsl
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
// Screen-space reflections over the pre-tonemap HDR scene plane.
|
||||
//
|
||||
// Inputs come from two producers:
|
||||
// - scene.hlsl ps_refl_gbuf: a half-res reflection G-buffer re-rendered
|
||||
// from the frame's reflective draw items only (env families 5/6/13 +
|
||||
// water). RG = octahedral-encoded WORLD-space normal, B = linear view
|
||||
// depth, A = reflectivity weight, NEGATED for surfaces that never write
|
||||
// scene depth (water / blended fam-13 glass); those use an in-front
|
||||
// visibility rule instead of the exact depth match.
|
||||
// - a full-res linear view-Z plane (the SSAO linearize output when AO ran
|
||||
// this frame, else ps_linearize below).
|
||||
//
|
||||
// Pass chain (own binding layout: b0 root constants + t0/t1/t2 single-
|
||||
// texture tables + point/linear clamp samplers):
|
||||
// ps_linearize scene depth -> view Z, R32_FLOAT full res (only when the
|
||||
// SSAO pass did not already produce the plane this frame)
|
||||
// ps_march half res: reconstruct the view-space ray, reflect about
|
||||
// the G-buffer normal, march the full-res depth plane and
|
||||
// sample the HDR scene at the hit. rgb = reflected color,
|
||||
// a = ray confidence (facing / edge / precision fades).
|
||||
// ps_composite full res onto the HDR scene plane (src-alpha blend):
|
||||
// a = confidence x reflectivity x intensity. The material's
|
||||
// static cube-reflection term stays in the scene as the
|
||||
// fallback; SSR blends over it where a ray hit.
|
||||
//
|
||||
// Spaces: the march runs in the SSAO passes' "AO view space", positions
|
||||
// from ViewPos(uv, z) built with |m00|/|m11|, i.e. the real view space with
|
||||
// each axis mirrored by its projection sign (the guest projection has
|
||||
// NEGATIVE m00). Reflection directions are mirror-SENSITIVE, so vrow0..2
|
||||
// fold the same axis signs into the world->view normal rotation; ray
|
||||
// geometry and screen projection then stay consistent by construction.
|
||||
cbuffer C : register(b0) {
|
||||
float4 size; // xy = dest size in px, zw = 1 / size
|
||||
float4 proj; // x = |m00|, y = |m11|, z = m22, w = m32 (row-vector proj)
|
||||
float4 vrow0; // world->AO-view rotation rows: n_view = n.x * vrow0.xyz
|
||||
float4 vrow1; // + n.y * vrow1.xyz + n.z * vrow2.xyz
|
||||
float4 vrow2;
|
||||
float4 p0; // x = march steps, y = thickness (view units),
|
||||
// z = composite intensity, w = debug mode
|
||||
float4 p1; // x = max ray distance (view units)
|
||||
float4 p2; // spare
|
||||
};
|
||||
// Per-pass t0/t1/t2 roles (each entry uses one of the overlapping
|
||||
// declarations; the rest drop out at compile):
|
||||
// ps_linearize t0 = scene depth (2D or 2DMS under SSR_MSAA)
|
||||
// ps_march t0 = G-buffer, t1 = linear depth, t2 = HDR scene
|
||||
// ps_composite t0 = SSR result, t1 = G-buffer, t2 = linear depth
|
||||
#ifdef SSR_MSAA
|
||||
Texture2DMS<float> depth_ms : register(t0);
|
||||
#endif
|
||||
Texture2D<float> tex0d : register(t0);
|
||||
Texture2D<float4> tex0 : register(t0);
|
||||
Texture2D<float> tex1 : register(t1);
|
||||
Texture2D<float4> tex1c : register(t1);
|
||||
Texture2D<float4> tex2 : register(t2);
|
||||
Texture2D<float> tex2d : register(t2);
|
||||
SamplerState smp_point : register(s0);
|
||||
SamplerState smp_linear : 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;
|
||||
}
|
||||
|
||||
// Device depth -> view-space Z. Row-vector projection with m23 = 1:
|
||||
// ndc_z = m22 + m32 / z_view.
|
||||
float LinearZ(float d) {
|
||||
return clamp(proj.w / min(d - proj.z, -1e-6), 0.05f, 4e5f);
|
||||
}
|
||||
|
||||
float ps_linearize(VSOut i) : SV_Target {
|
||||
#ifdef SSR_MSAA
|
||||
float d = depth_ms.Load(int2(i.pos.xy), 0);
|
||||
#else
|
||||
float d = tex0d.Load(int3(int2(i.pos.xy), 0));
|
||||
#endif
|
||||
return LinearZ(d);
|
||||
}
|
||||
|
||||
// AO-view-space position from UV + linear Z (the |m00|/|m11| mirrored view
|
||||
// space every SSR quantity lives in; see the header note).
|
||||
float3 ViewPos(float2 uv, float z) {
|
||||
float2 ndc = float2(uv.x * 2.0f - 1.0f, 1.0f - uv.y * 2.0f);
|
||||
return float3(ndc.x * z / proj.x, ndc.y * z / proj.y, z);
|
||||
}
|
||||
|
||||
// AO-view-space position -> screen UV (the exact ViewPos inverse).
|
||||
float2 ViewUv(float3 q) {
|
||||
float2 ndc = float2(q.x * proj.x / q.z, q.y * proj.y / q.z);
|
||||
return float2(ndc.x * 0.5f + 0.5f, 0.5f - ndc.y * 0.5f);
|
||||
}
|
||||
|
||||
// Octahedral normal decode (ps_refl_gbuf encodes; unit vectors only).
|
||||
float3 OctDecode(float2 e) {
|
||||
float3 n = float3(e.x, e.y, 1.0f - abs(e.x) - abs(e.y));
|
||||
if (n.z < 0.0f) {
|
||||
n.xy = float2((1.0f - abs(e.y)) * (e.x >= 0.0f ? 1.0f : -1.0f),
|
||||
(1.0f - abs(e.x)) * (e.y >= 0.0f ? 1.0f : -1.0f));
|
||||
}
|
||||
return normalize(n);
|
||||
}
|
||||
|
||||
// G-buffer visibility vs the scene depth: opaque reflective surfaces
|
||||
// (signed_w > 0) wrote the depth buffer, so their stored depth must MATCH
|
||||
// it; a mismatch means another surface occludes this G-buffer texel.
|
||||
// Water / blended glass (signed_w < 0) never write depth; they are visible
|
||||
// exactly when they sit IN FRONT of the opaque depth.
|
||||
// grazing = 1 - |N.V|: on surfaces seen edge-on the view depth changes by
|
||||
// meters across one half-res G-buffer texel, so the match window must widen
|
||||
// with slope; a fixed window rejects valid texels in a dither pattern
|
||||
// (stippled reflection dropout concentrated on grazing panes).
|
||||
float GbufVisible(float gz, float sz, float signed_w, float grazing) {
|
||||
float eps = max(gz * 0.04f, 0.3f) * (1.0f + 8.0f * grazing * grazing);
|
||||
if (signed_w < 0.0f) {
|
||||
return gz < sz + eps ? 1.0f : 0.0f;
|
||||
}
|
||||
return abs(gz - sz) < eps ? 1.0f : 0.0f;
|
||||
}
|
||||
|
||||
float4 ps_march(VSOut i) : SV_Target {
|
||||
// Debug 4 (ray autopsy): color-code where each ray terminates;
|
||||
// green = G-buffer visibility rejected, blue = degenerate direction,
|
||||
// magenta = crossed the near plane, cyan = left the screen,
|
||||
// yellow = ran out of steps with no crossing, red = crossing found but
|
||||
// thicker than the window every time, white x conf = accepted hit.
|
||||
const bool autopsy = p0.w > 3.5f;
|
||||
float4 g = tex0.SampleLevel(smp_point, i.uv, 0);
|
||||
float wrefl = abs(g.a);
|
||||
float gz = g.b;
|
||||
if (wrefl < 0.004f) {
|
||||
return float4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
float3 P = ViewPos(i.uv, gz);
|
||||
float3 nw = OctDecode(g.rg);
|
||||
float3 N = normalize(nw.x * vrow0.xyz + nw.y * vrow1.xyz +
|
||||
nw.z * vrow2.xyz);
|
||||
float3 V = normalize(P);
|
||||
float grazing = 1.0f - saturate(abs(dot(N, V)));
|
||||
float sz = tex1.SampleLevel(smp_point, i.uv, 0);
|
||||
if (GbufVisible(gz, sz, g.a, grazing) < 0.5f) {
|
||||
return autopsy ? float4(0.0f, 1.0f, 0.0f, 1.0f)
|
||||
: float4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
float3 R = reflect(V, N);
|
||||
// Toward-camera rays are allowed: a mirror facing the viewer bounces
|
||||
// exactly that way, and the march still resolves hits against the depth
|
||||
// buffer; the reflected subject is painted from its camera-facing
|
||||
// texels (the standard screen-space approximation; this is what puts the
|
||||
// skater in the glass). Only rays aimed at the camera POSITION itself
|
||||
// are degenerate (they reflect the viewpoint, which has no on-screen
|
||||
// content); fade those out instead of popping.
|
||||
// Keep this gate TIGHT: the self-reflection zone (skater in the glass)
|
||||
// has ray directions within ~25 degrees of the to-camera axis, and a wide
|
||||
// fade here erases it wholesale (the autopsy's blue disc). Only rays
|
||||
// within a few degrees of the camera point itself are unresolvable.
|
||||
float degen = 1.0f - smoothstep(0.95f, 0.995f, dot(R, -V));
|
||||
if (degen <= 0.0f) {
|
||||
return autopsy ? float4(0.0f, 0.0f, 1.0f, 1.0f)
|
||||
: float4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
// Lift the origin off the surface (depth-proportional) so grazing rays
|
||||
// don't re-hit their own G-buffer texel as instant self-intersections.
|
||||
P += N * (0.02f + gz * 0.004f);
|
||||
|
||||
// ---- Screen-space DDA march ----
|
||||
// The ray is clipped in VIEW space so its far endpoint stays in front of
|
||||
// the near plane, then its SCREEN projection is walked with UNIFORM
|
||||
// screen-space steps. 1/z is exactly linear along the projection of a 3D
|
||||
// line, so the ray depth at every tap interpolates perspective-correctly
|
||||
// - unlike view-space stepping, no stride can overshoot a depth feature
|
||||
// wider than one screen step (the failure mode that made hits sparse and
|
||||
// demanded thickness/jitter crutches).
|
||||
float t_far = p1.x;
|
||||
if (R.z < 0.0f) {
|
||||
t_far = min(t_far, (P.z - 0.2f) / -R.z); // stop before the near plane
|
||||
}
|
||||
t_far = max(t_far, 0.05f);
|
||||
float3 Q1 = P + R * t_far;
|
||||
float2 uv0 = ViewUv(P);
|
||||
float2 uv1 = ViewUv(Q1);
|
||||
float2 dseg = uv1 - uv0;
|
||||
// Clip the screen segment to the viewport rectangle.
|
||||
float s_max = 1.0f;
|
||||
if (dseg.x > 1e-6f) {
|
||||
s_max = min(s_max, (1.0f - uv0.x) / dseg.x);
|
||||
} else if (dseg.x < -1e-6f) {
|
||||
s_max = min(s_max, -uv0.x / dseg.x);
|
||||
}
|
||||
if (dseg.y > 1e-6f) {
|
||||
s_max = min(s_max, (1.0f - uv0.y) / dseg.y);
|
||||
} else if (dseg.y < -1e-6f) {
|
||||
s_max = min(s_max, -uv0.y / dseg.y);
|
||||
}
|
||||
if (s_max <= 0.0f || dot(dseg, dseg) < 1e-10f) {
|
||||
return autopsy ? float4(0.0f, 1.0f, 1.0f, 1.0f)
|
||||
: float4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
float w0 = 1.0f / P.z;
|
||||
float w1 = 1.0f / Q1.z;
|
||||
float steps = max(p0.x, 8.0f);
|
||||
// Small phase jitter; the composite's confidence-weighted blur and the
|
||||
// bilinear upsample integrate it. Kept small: per-pixel phase error in
|
||||
// the HIT POSITION runs along the ray's screen direction and reads as
|
||||
// diagonal smearing of the reflected image.
|
||||
float jitter = 0.5f * frac(52.9829189f *
|
||||
frac(dot(i.pos.xy,
|
||||
float2(0.06711056f, 0.00583715f))));
|
||||
float s_prev = 0.0f;
|
||||
float hit_s = -1.0f;
|
||||
float hit_z = 0.0f;
|
||||
float hit_dz = 0.0f;
|
||||
float2 hit_uv = float2(0.0f, 0.0f);
|
||||
float3 death = float3(1.0f, 1.0f, 0.0f); // autopsy: default = out of steps
|
||||
[loop] for (int k = 0; k < 64; ++k) {
|
||||
if (k >= (int)steps) {
|
||||
break;
|
||||
}
|
||||
float s = (float(k) + 0.5f + jitter) / steps * s_max;
|
||||
float2 uv = uv0 + dseg * s;
|
||||
float z_ray = 1.0f / lerp(w0, w1, s);
|
||||
float zs = tex1.SampleLevel(smp_point, uv, 0);
|
||||
float dz = z_ray - zs;
|
||||
if (dz > 0.0f) {
|
||||
// Depth crossing: refine in s (same perspective-correct interp),
|
||||
// then judge the refined gap against the thickness window.
|
||||
hit_s = s;
|
||||
hit_uv = uv;
|
||||
hit_z = z_ray;
|
||||
hit_dz = dz;
|
||||
float lo = s_prev;
|
||||
float hi = s;
|
||||
// 6 bisections: a full-screen segment at 48 taps has ~40 half-res px
|
||||
// strides; /64 refinement pins the hit to sub-pixel so the sampled
|
||||
// reflection doesn't wander along the ray (diagonal smear).
|
||||
[unroll] for (int r = 0; r < 6; ++r) {
|
||||
float sm = 0.5f * (lo + hi);
|
||||
float2 um = uv0 + dseg * sm;
|
||||
float zr = 1.0f / lerp(w0, w1, sm);
|
||||
float zm = tex1.SampleLevel(smp_point, um, 0);
|
||||
float dm = zr - zm;
|
||||
if (dm > 0.0f) {
|
||||
hi = sm;
|
||||
hit_s = sm;
|
||||
hit_uv = um;
|
||||
hit_z = zr;
|
||||
hit_dz = dm;
|
||||
} else {
|
||||
lo = sm;
|
||||
}
|
||||
}
|
||||
float thick = p0.y * (1.0f + hit_z * 0.03f);
|
||||
if (hit_dz < thick) {
|
||||
break; // real surface hit
|
||||
}
|
||||
hit_s = -1.0f; // passed behind a silhouette: keep marching
|
||||
death = float3(1.0f, 0.0f, 0.0f);
|
||||
}
|
||||
s_prev = s;
|
||||
}
|
||||
if (hit_s < 0.0f) {
|
||||
return autopsy ? float4(death, 1.0f) : float4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
// Confidence: the tight degenerate fade and a screen-edge fade at the hit
|
||||
// (content leaving the screen hands off to the cube term smoothly). The
|
||||
// DDA needs no distance or precision shaping; a hit is a hit.
|
||||
float2 eb = min(hit_uv, 1.0f - hit_uv);
|
||||
float edge = saturate(min(eb.x, eb.y) * 12.0f);
|
||||
float conf = degen * edge;
|
||||
if (autopsy) {
|
||||
return float4(conf, conf, conf, 1.0f); // accepted hit: white x conf
|
||||
}
|
||||
// Firefly clamp: the scene plane is pre-tonemap HDR; reflected sun-lit
|
||||
// texels above the tone curve's range sparkle hard at half res.
|
||||
float3 col = min(tex2.SampleLevel(smp_linear, hit_uv, 0).rgb, 3.0f);
|
||||
return float4(col, conf);
|
||||
}
|
||||
|
||||
float4 ps_composite(VSOut i) : SV_Target {
|
||||
float4 g = tex1c.SampleLevel(smp_point, i.uv, 0);
|
||||
float wrefl = abs(g.a);
|
||||
float dbg = p0.w;
|
||||
if (dbg > 1.5f && dbg < 2.5f) {
|
||||
// Debug 2: G-buffer world normals (black where no reflective surface).
|
||||
float3 nw = wrefl < 0.004f ? float3(-1.0f, -1.0f, -1.0f) : OctDecode(g.rg);
|
||||
return float4(nw * 0.5f + 0.5f, 1.0f);
|
||||
}
|
||||
if (dbg > 3.5f) {
|
||||
// Debug 4: the march's ray-autopsy plane, raw.
|
||||
return float4(tex0.SampleLevel(smp_point, i.uv, 0).rgb, 1.0f);
|
||||
}
|
||||
float sz = tex2d.SampleLevel(smp_point, i.uv, 0);
|
||||
float vis = 0.0f;
|
||||
if (wrefl >= 0.004f) {
|
||||
float3 n = normalize(OctDecode(g.rg).x * vrow0.xyz +
|
||||
OctDecode(g.rg).y * vrow1.xyz +
|
||||
OctDecode(g.rg).z * vrow2.xyz);
|
||||
float3 v = normalize(ViewPos(i.uv, g.b));
|
||||
vis = GbufVisible(g.b, sz, g.a, 1.0f - saturate(abs(dot(n, v))));
|
||||
}
|
||||
if (dbg > 2.5f) {
|
||||
// Debug 3: reflectivity (red) x depth visibility (green).
|
||||
return float4(wrefl, vis, 0.0f, 1.0f);
|
||||
}
|
||||
// Confidence-weighted 5-tap cross over the half-res SSR plane: the
|
||||
// jittered march resolves per-pixel hit phase noise (reflected thin
|
||||
// geometry over bright content sparkles otherwise); weighting by
|
||||
// confidence keeps misses from bleeding black into hits.
|
||||
float2 hs = size.zw; // one texel of the half-res SSR plane
|
||||
float4 s0 = tex0.SampleLevel(smp_linear, i.uv, 0);
|
||||
float4 s1 = tex0.SampleLevel(smp_linear, i.uv + float2(hs.x, 0.0f), 0);
|
||||
float4 s2 = tex0.SampleLevel(smp_linear, i.uv - float2(hs.x, 0.0f), 0);
|
||||
float4 s3 = tex0.SampleLevel(smp_linear, i.uv + float2(0.0f, hs.y), 0);
|
||||
float4 s4 = tex0.SampleLevel(smp_linear, i.uv - float2(0.0f, hs.y), 0);
|
||||
float wsum = s0.a * 6.0f + s1.a + s2.a + s3.a + s4.a;
|
||||
float4 ssr;
|
||||
ssr.rgb = wsum > 1e-4f
|
||||
? (s0.rgb * s0.a * 6.0f + s1.rgb * s1.a + s2.rgb * s2.a +
|
||||
s3.rgb * s3.a + s4.rgb * s4.a) / wsum
|
||||
: s0.rgb;
|
||||
ssr.a = wsum * (1.0f / 10.0f);
|
||||
if (dbg > 0.5f) {
|
||||
// Debug 1: raw SSR color, confidence as a blue lift where colorless.
|
||||
return float4(ssr.rgb + float3(0.0f, 0.0f, 0.15f * ssr.a), 1.0f);
|
||||
}
|
||||
return float4(ssr.rgb, saturate(ssr.a * wrefl * vis * p0.z));
|
||||
}
|
||||
|
|
@ -135,6 +135,42 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_ssao_full_res, false, "Skate 3",
|
|||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_ssao_debug, false, "Skate 3",
|
||||
"Replace the frame with the SSAO visibility term (tuning aid).")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_ssr, false, "Skate 3",
|
||||
"Screen-space reflections on reflective glass and water: "
|
||||
"on-screen scenery reflected in real time where the original "
|
||||
"game shows only a static environment cube. Rays that miss "
|
||||
"(off-screen content, screen edges) fade back to the cube "
|
||||
"term. Requires the HDR scene intermediate. EXPERIMENTAL: "
|
||||
"off by default pending image-quality calibration (residual "
|
||||
"smearing/noise on some surfaces).")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_INT32(skate3_native_render_scene_ssr_steps, 48, "Skate 3",
|
||||
"SSR screen-space march taps per ray (half-res pass). More "
|
||||
"taps resolve thinner reflected geometry at proportional "
|
||||
"GPU cost.")
|
||||
.range(8, 64)
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_ssr_thickness, 1.0, "Skate 3",
|
||||
"SSR depth thickness (view units): how deep behind a "
|
||||
"visible surface a marching ray still counts as a hit. "
|
||||
"Too small and rays tunnel through thin ledges/rails; too "
|
||||
"large and reflections smear under overhangs.")
|
||||
.range(0.05, 8.0)
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_ssr_intensity, 1.0, "Skate 3",
|
||||
"SSR composite strength over the material's cube "
|
||||
"reflection term (0 = effectively off).")
|
||||
.range(0.0, 2.0)
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_INT32(skate3_native_render_scene_ssr_debug, 0, "Skate 3",
|
||||
"SSR debug view: 1 = raw ray color + confidence, 2 = "
|
||||
"reflection G-buffer normals, 3 = reflectivity (red) x "
|
||||
"depth visibility (green), 4 = ray autopsy (green = "
|
||||
"visibility reject, blue = degenerate, cyan = off-screen, "
|
||||
"magenta = near plane, yellow = out of steps, red = "
|
||||
"too-thick crossings, white = hit x confidence).")
|
||||
.range(0, 4)
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_hdr, true, "Skate 3",
|
||||
"Render the 3D scene into a float (HDR) intermediate and apply the "
|
||||
"game's shared tone chain once in a host post pass, the basis for "
|
||||
|
|
@ -10315,6 +10351,45 @@ struct RendererState {
|
|||
// Scene float format the output-sized targets were built with (kUnknown
|
||||
// when classic); packed-format toggles rebuild them.
|
||||
nrhi::Format targets_scene_fmt = nrhi::Format::kUnknown;
|
||||
// Screen-space reflections (ssr.hlsl + scene.hlsl ps_refl_gbuf): a
|
||||
// half-res reflection G-buffer re-rendered from the frame's reflective
|
||||
// items (env fams 5/6/13 + water), a half-res screen-space march over the
|
||||
// full-res linear depth sampling the pre-tonemap HDR plane, and a
|
||||
// src-alpha composite back onto it between the AO pass and the HDR post
|
||||
// (so reflections tonemap/bloom like directly-visible scenery). HDR path
|
||||
// only. Post binding layout = the SSAO shape with 32 root floats + three
|
||||
// single-texture tables; the G-buffer pass runs under the MAIN layout
|
||||
// (scene VS + per-item constants). All intermediates idle in
|
||||
// RENDER_TARGET state.
|
||||
nrhi::BindingLayout* ssr_layout = nullptr;
|
||||
nrhi::Pipeline* pso_ssr_gbuf = nullptr; // scene-VS geometry pass
|
||||
nrhi::Pipeline* pso_ssr_linearize = nullptr; // fallback when SSAO idle
|
||||
nrhi::Pipeline* pso_ssr_march = nullptr;
|
||||
nrhi::Pipeline* pso_ssr_composite = nullptr;
|
||||
nrhi::Texture* ssr_gbuf = nullptr; // half res RGBA16F reflection G-buffer
|
||||
// Half-res depth for the G-buffer pass (test+write LESS): reflective
|
||||
// items overlap themselves in screen space (a curved glass tower's far
|
||||
// side shares texels with its near side inside ONE mesh), so without a
|
||||
// depth test draw/triangle order decides which surface owns a texel and
|
||||
// whole visible panes fail the march's scene-depth match. Idles in
|
||||
// DEPTH_WRITE state.
|
||||
nrhi::Texture* ssr_gbuf_depth = nullptr;
|
||||
nrhi::Texture* ssr_tex = nullptr; // half res RGBA16F march output
|
||||
nrhi::Texture* ssr_lin_depth = nullptr; // full res R32F (own linearize)
|
||||
nrhi::TextureView* ssr_gbuf_srv = nullptr;
|
||||
nrhi::TextureView* ssr_srv = nullptr;
|
||||
nrhi::TextureView* ssr_lin_srv = nullptr;
|
||||
// Scene-depth SRV for the own linearize, re-pointed on depth rebuilds.
|
||||
nrhi::TextureView* ssr_depth_srv = nullptr;
|
||||
nrhi::Texture* ssr_depth_srv_of = nullptr;
|
||||
uint32_t ssr_width = 0, ssr_height = 0; // half-res raster
|
||||
uint32_t ssr_lin_width = 0, ssr_lin_height = 0;
|
||||
uint32_t ssr_msaa = 0; // linearize-variant PSO latch
|
||||
nrhi::Format ssr_scene_fmt = nrhi::Format::kUnknown; // composite target
|
||||
bool ssr_failed = false;
|
||||
// Per-frame handoff: the G-buffer pass drew and left ssr_gbuf in
|
||||
// PIXEL_SHADER_RESOURCE for ApplySsrPass (which consumes + restores it).
|
||||
bool ssr_gbuf_ready = false;
|
||||
std::unordered_map<uint32_t, MeshBuffers> meshes;
|
||||
// (The old D3D12 bookkeeping, the retired-resource vector, the CPU SRV
|
||||
// staging heap and its slot allocator/recycling lists, is gone: resource
|
||||
|
|
@ -14245,6 +14320,406 @@ bool ApplySsaoPass(const NativeGuestOutputRenderContext& context,
|
|||
return true;
|
||||
}
|
||||
|
||||
// ---- Screen-space reflections (ssr.hlsl + scene.hlsl ps_refl_gbuf) --------
|
||||
// Layout + PSOs, built lazily on the first SSR frame and rebuilt when the
|
||||
// MSAA level (linearize variant) or the scene float format (composite
|
||||
// target) changes. Failure disables the effect only, never g_r.failed.
|
||||
bool EnsureSsrPipeline(const NativeGuestOutputRenderContext& context) {
|
||||
if (g_r.ssr_failed || !g_r.hdr_active) {
|
||||
return false;
|
||||
}
|
||||
if (g_r.pso_ssr_march != nullptr && g_r.ssr_msaa == g_r.msaa &&
|
||||
g_r.ssr_scene_fmt == g_r.hdr_scene_format) {
|
||||
return true;
|
||||
}
|
||||
nrhi::Device* device = context.device;
|
||||
const auto fail = [&](const char* what) {
|
||||
REXLOG_ERROR("native-scene: ssr pipeline setup failed ({})", what);
|
||||
g_r.ssr_failed = true;
|
||||
return false;
|
||||
};
|
||||
if (g_r.ssr_layout == nullptr) {
|
||||
// The SSAO layout shape with a wider constant block (the march needs
|
||||
// the world->view rotation rows): root constants b0 (32 floats) + three
|
||||
// single-texture tables + point/linear clamp samplers.
|
||||
nrhi::BindingLayoutDesc ld;
|
||||
ld.param_count = 4;
|
||||
ld.params[0] = {nrhi::BindingParamKind::kConstants, /*b*/ 0, 32,
|
||||
nrhi::Visibility::kAll};
|
||||
ld.params[1] = {nrhi::BindingParamKind::kTextureTable, /*t*/ 0, 1,
|
||||
nrhi::Visibility::kPixel};
|
||||
ld.params[2] = {nrhi::BindingParamKind::kTextureTable, 1, 1,
|
||||
nrhi::Visibility::kPixel};
|
||||
ld.params[3] = {nrhi::BindingParamKind::kTextureTable, 2, 1,
|
||||
nrhi::Visibility::kPixel};
|
||||
ld.static_sampler_count = 2;
|
||||
ld.static_samplers[0] = {/*s*/ 0, nrhi::Filter::kPoint,
|
||||
nrhi::AddressMode::kClamp, 1};
|
||||
ld.static_samplers[1] = {1, nrhi::Filter::kLinear,
|
||||
nrhi::AddressMode::kClamp, 1};
|
||||
ld.allow_input_layout = false;
|
||||
g_r.ssr_layout = device->CreateBindingLayout(ld);
|
||||
if (g_r.ssr_layout == nullptr) {
|
||||
return fail("binding layout");
|
||||
}
|
||||
}
|
||||
for (nrhi::Pipeline** p : {&g_r.pso_ssr_gbuf, &g_r.pso_ssr_linearize,
|
||||
&g_r.pso_ssr_march, &g_r.pso_ssr_composite}) {
|
||||
if (*p != nullptr) {
|
||||
device->DestroyDeferred(*p);
|
||||
*p = nullptr;
|
||||
}
|
||||
}
|
||||
const bool msaa = g_r.msaa > 1;
|
||||
const nrhi::ShaderMacro msaa_defs[] = {{"SSR_MSAA", "1"},
|
||||
{nullptr, nullptr}};
|
||||
nrhi::Shader* scene_vs = device->CreateShader(
|
||||
MakeShaderDesc(nrhi::ShaderStage::kVertex, "scene.hlsl", kShaderSource,
|
||||
"vs_main", nullptr, ""));
|
||||
nrhi::Shader* ps_gbuf = device->CreateShader(
|
||||
MakeShaderDesc(nrhi::ShaderStage::kPixel, "scene.hlsl", kShaderSource,
|
||||
"ps_refl_gbuf", nullptr, ""));
|
||||
nrhi::Shader* vs = device->CreateShader(
|
||||
MakeShaderDesc(nrhi::ShaderStage::kVertex, "ssr.hlsl", kSsrShaderSource,
|
||||
"vs_main", nullptr, ""));
|
||||
nrhi::Shader* ps_lin = device->CreateShader(MakeShaderDesc(
|
||||
nrhi::ShaderStage::kPixel, "ssr.hlsl", kSsrShaderSource, "ps_linearize",
|
||||
msaa ? msaa_defs : nullptr, msaa ? "SSR_MSAA=1" : ""));
|
||||
nrhi::Shader* ps_march = device->CreateShader(
|
||||
MakeShaderDesc(nrhi::ShaderStage::kPixel, "ssr.hlsl", kSsrShaderSource,
|
||||
"ps_march", nullptr, ""));
|
||||
nrhi::Shader* ps_comp = device->CreateShader(
|
||||
MakeShaderDesc(nrhi::ShaderStage::kPixel, "ssr.hlsl", kSsrShaderSource,
|
||||
"ps_composite", nullptr, ""));
|
||||
const bool shaders_ok = scene_vs != nullptr && ps_gbuf != nullptr &&
|
||||
vs != nullptr && ps_lin != nullptr &&
|
||||
ps_march != nullptr && ps_comp != nullptr;
|
||||
if (!shaders_ok) {
|
||||
for (nrhi::Shader* s :
|
||||
{scene_vs, ps_gbuf, vs, ps_lin, ps_march, ps_comp}) {
|
||||
device->DestroyDeferred(s);
|
||||
}
|
||||
return fail("shader compile");
|
||||
}
|
||||
// Reflection G-buffer pass: the scene VS + input layout under the MAIN
|
||||
// binding layout (the caller re-stages each reflective item's main-pass
|
||||
// root constants / textures), half-res single-sample float target with
|
||||
// its OWN half-res depth (test+write: the MSAA scene depth cannot pair
|
||||
// with a 1x RTV) so the nearest reflective surface owns each texel;
|
||||
// occlusion by non-reflective geometry is then resolved in the march by
|
||||
// comparing the stored G-buffer depth against the scene linear depth.
|
||||
{
|
||||
nrhi::GraphicsPipelineDesc pso;
|
||||
pso.layout = g_r.layout;
|
||||
pso.vs = scene_vs;
|
||||
pso.ps = ps_gbuf;
|
||||
pso.input_elements = kSceneInputLayout;
|
||||
pso.input_element_count = 7;
|
||||
pso.vertex_stride = 56;
|
||||
pso.cull = nrhi::CullMode::kNone;
|
||||
pso.sample_count = 1;
|
||||
pso.depth_clip = true;
|
||||
pso.depth.test_enable = true;
|
||||
pso.depth.write_enable = true;
|
||||
pso.depth.func = nrhi::CompareFunc::kLess;
|
||||
pso.dsv_format = nrhi::Format::kD32_FLOAT;
|
||||
pso.rtv_format = nrhi::Format::kR16G16B16A16_FLOAT;
|
||||
g_r.pso_ssr_gbuf = device->CreateGraphicsPipeline(pso);
|
||||
}
|
||||
nrhi::GraphicsPipelineDesc pso;
|
||||
pso.layout = g_r.ssr_layout;
|
||||
pso.vs = vs;
|
||||
pso.cull = nrhi::CullMode::kNone;
|
||||
pso.sample_count = 1;
|
||||
pso.ps = ps_lin;
|
||||
pso.rtv_format = nrhi::Format::kR32_FLOAT;
|
||||
g_r.pso_ssr_linearize = device->CreateGraphicsPipeline(pso);
|
||||
pso.ps = ps_march;
|
||||
pso.rtv_format = nrhi::Format::kR16G16B16A16_FLOAT;
|
||||
g_r.pso_ssr_march = device->CreateGraphicsPipeline(pso);
|
||||
// Composite: straight src-alpha blend onto the float scene plane (the
|
||||
// shader outputs confidence x reflectivity x intensity in alpha), RGB
|
||||
// write mask: the plane's alpha stays untouched.
|
||||
pso.ps = ps_comp;
|
||||
pso.rtv_format = g_r.hdr_scene_format;
|
||||
pso.blend.enable = true;
|
||||
pso.blend.src = nrhi::BlendFactor::kSrcAlpha;
|
||||
pso.blend.dst = nrhi::BlendFactor::kInvSrcAlpha;
|
||||
pso.blend.op = nrhi::BlendOp::kAdd;
|
||||
pso.blend.src_alpha = nrhi::BlendFactor::kZero;
|
||||
pso.blend.dst_alpha = nrhi::BlendFactor::kOne;
|
||||
pso.blend.op_alpha = nrhi::BlendOp::kAdd;
|
||||
pso.blend.write_mask = 0x7;
|
||||
g_r.pso_ssr_composite = device->CreateGraphicsPipeline(pso);
|
||||
for (nrhi::Shader* s : {scene_vs, ps_gbuf, vs, ps_lin, ps_march, ps_comp}) {
|
||||
device->DestroyDeferred(s);
|
||||
}
|
||||
if (g_r.pso_ssr_gbuf == nullptr || g_r.pso_ssr_linearize == nullptr ||
|
||||
g_r.pso_ssr_march == nullptr || g_r.pso_ssr_composite == nullptr) {
|
||||
return fail("pso");
|
||||
}
|
||||
g_r.ssr_msaa = g_r.msaa;
|
||||
g_r.ssr_scene_fmt = g_r.hdr_scene_format;
|
||||
REXLOG_INFO("native-scene: ssr pipeline created (MSAA x{})", g_r.msaa);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Half-res SSR intermediates (reflection G-buffer + march output), steady
|
||||
// state RENDER_TARGET, rebuilt on resize.
|
||||
bool EnsureSsrTargets(const NativeGuestOutputRenderContext& context) {
|
||||
const uint32_t sw = std::max(1u, (context.guest_output_width + 1) / 2);
|
||||
const uint32_t sh = std::max(1u, (context.guest_output_height + 1) / 2);
|
||||
if (g_r.ssr_width == sw && g_r.ssr_height == sh && g_r.ssr_gbuf != nullptr &&
|
||||
g_r.ssr_tex != nullptr) {
|
||||
return true;
|
||||
}
|
||||
nrhi::Device* device = context.device;
|
||||
if (g_r.ssr_gbuf_depth != nullptr) {
|
||||
device->DestroyDeferred(g_r.ssr_gbuf_depth);
|
||||
g_r.ssr_gbuf_depth = nullptr;
|
||||
}
|
||||
{
|
||||
nrhi::TextureDesc dd;
|
||||
dd.width = sw;
|
||||
dd.height = sh;
|
||||
dd.format = nrhi::Format::kD32_FLOAT;
|
||||
dd.usage = nrhi::kTextureUsageDepthStencil;
|
||||
dd.initial_state = nrhi::ResourceState::kDepthWrite;
|
||||
dd.clear_depth = 1.0f;
|
||||
g_r.ssr_gbuf_depth = device->CreateTexture(dd);
|
||||
if (g_r.ssr_gbuf_depth == nullptr) {
|
||||
g_r.ssr_failed = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
nrhi::Texture** res[2] = {&g_r.ssr_gbuf, &g_r.ssr_tex};
|
||||
nrhi::TextureView** views[2] = {&g_r.ssr_gbuf_srv, &g_r.ssr_srv};
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
if (*views[i] != nullptr) {
|
||||
device->DestroyDeferred(*views[i]);
|
||||
*views[i] = nullptr;
|
||||
}
|
||||
if (*res[i] != nullptr) {
|
||||
device->DestroyDeferred(*res[i]);
|
||||
*res[i] = nullptr;
|
||||
}
|
||||
nrhi::TextureDesc desc;
|
||||
desc.width = sw;
|
||||
desc.height = sh;
|
||||
desc.mip_levels = 1;
|
||||
desc.format = nrhi::Format::kR16G16B16A16_FLOAT;
|
||||
desc.usage = nrhi::kTextureUsageRenderTarget;
|
||||
desc.initial_state = nrhi::ResourceState::kRenderTarget;
|
||||
*res[i] = device->CreateTexture(desc);
|
||||
if (*res[i] == nullptr) {
|
||||
g_r.ssr_failed = true;
|
||||
return false;
|
||||
}
|
||||
nrhi::TextureViewDesc vd;
|
||||
vd.mip_levels = 1;
|
||||
*views[i] = device->CreateTextureView(*res[i], vd);
|
||||
if (*views[i] == nullptr) {
|
||||
g_r.ssr_failed = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
g_r.ssr_width = sw;
|
||||
g_r.ssr_height = sh;
|
||||
return true;
|
||||
}
|
||||
|
||||
// SSR march + composite (see ssr.hlsl): consumes the reflection G-buffer
|
||||
// the in-pass draw left in PIXEL_SHADER_RESOURCE, marches the full-res
|
||||
// linear depth (reusing the SSAO linearize plane when AO ran this frame)
|
||||
// while sampling the pre-tonemap HDR plane, and blends the result back onto
|
||||
// it. Returns true when it drew; the binding layout changed and the
|
||||
// caller's post_ran block restores the main-pass bindings.
|
||||
bool ApplySsrPass(const NativeGuestOutputRenderContext& context,
|
||||
nrhi::Cmd* cmd, const FrameScene& scene,
|
||||
const nrhi::Viewport& viewport, const nrhi::Rect& scissor,
|
||||
bool lin_depth_ready) {
|
||||
if (!g_r.ssr_gbuf_ready) {
|
||||
return false;
|
||||
}
|
||||
// Consume the handoff up front: every exit below must leave the G-buffer
|
||||
// back in its RENDER_TARGET steady state.
|
||||
g_r.ssr_gbuf_ready = false;
|
||||
nrhi::Device* device = context.device;
|
||||
const uint32_t width = context.guest_output_width;
|
||||
const uint32_t height = context.guest_output_height;
|
||||
const auto bail = [&] {
|
||||
cmd->Barrier(g_r.ssr_gbuf, nrhi::ResourceState::kPixelShaderResource,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
cmd->FlushBarriers();
|
||||
return false;
|
||||
};
|
||||
// Full-res linear view-Z: the SSAO plane when AO linearized this frame
|
||||
// (it idles back in RENDER_TARGET after the AO pass), else the own
|
||||
// linearize below.
|
||||
lin_depth_ready = lin_depth_ready && g_r.ao_lin_depth != nullptr &&
|
||||
g_r.ao_lin_srv != nullptr && g_r.ao_lin_width == width &&
|
||||
g_r.ao_lin_height == height;
|
||||
if (!lin_depth_ready) {
|
||||
if (g_r.ssr_lin_depth == nullptr || g_r.ssr_lin_width != width ||
|
||||
g_r.ssr_lin_height != height) {
|
||||
if (g_r.ssr_lin_srv != nullptr) {
|
||||
device->DestroyDeferred(g_r.ssr_lin_srv);
|
||||
g_r.ssr_lin_srv = nullptr;
|
||||
}
|
||||
if (g_r.ssr_lin_depth != nullptr) {
|
||||
device->DestroyDeferred(g_r.ssr_lin_depth);
|
||||
g_r.ssr_lin_depth = nullptr;
|
||||
}
|
||||
nrhi::TextureDesc desc;
|
||||
desc.width = width;
|
||||
desc.height = height;
|
||||
desc.mip_levels = 1;
|
||||
desc.format = nrhi::Format::kR32_FLOAT;
|
||||
desc.usage = nrhi::kTextureUsageRenderTarget;
|
||||
desc.initial_state = nrhi::ResourceState::kRenderTarget;
|
||||
g_r.ssr_lin_depth = device->CreateTexture(desc);
|
||||
if (g_r.ssr_lin_depth == nullptr) {
|
||||
g_r.ssr_failed = true;
|
||||
return bail();
|
||||
}
|
||||
nrhi::TextureViewDesc vd;
|
||||
vd.mip_levels = 1;
|
||||
g_r.ssr_lin_srv = device->CreateTextureView(g_r.ssr_lin_depth, vd);
|
||||
if (g_r.ssr_lin_srv == nullptr) {
|
||||
g_r.ssr_failed = true;
|
||||
return bail();
|
||||
}
|
||||
g_r.ssr_lin_width = width;
|
||||
g_r.ssr_lin_height = height;
|
||||
}
|
||||
// Scene-depth SRV, re-pointed when the depth buffer is rebuilt.
|
||||
if (g_r.ssr_depth_srv == nullptr || g_r.ssr_depth_srv_of != g_r.depth) {
|
||||
if (g_r.ssr_depth_srv != nullptr) {
|
||||
device->DestroyDeferred(g_r.ssr_depth_srv);
|
||||
g_r.ssr_depth_srv = nullptr;
|
||||
}
|
||||
nrhi::TextureViewDesc sd;
|
||||
if (g_r.msaa > 1) {
|
||||
sd.dimension = nrhi::ViewDimension::k2DMS;
|
||||
} else {
|
||||
sd.dimension = nrhi::ViewDimension::k2D;
|
||||
sd.mip_levels = 1;
|
||||
}
|
||||
g_r.ssr_depth_srv = device->CreateTextureView(g_r.depth, sd);
|
||||
if (g_r.ssr_depth_srv == nullptr) {
|
||||
g_r.ssr_failed = true;
|
||||
return bail();
|
||||
}
|
||||
g_r.ssr_depth_srv_of = g_r.depth;
|
||||
}
|
||||
}
|
||||
nrhi::Texture* const lin_tex =
|
||||
lin_depth_ready ? g_r.ao_lin_depth : g_r.ssr_lin_depth;
|
||||
nrhi::TextureView* const lin_srv =
|
||||
lin_depth_ready ? g_r.ao_lin_srv : g_r.ssr_lin_srv;
|
||||
|
||||
// b0 rows (see ssr.hlsl cbuffer C): dest size, projection, the world ->
|
||||
// "AO view space" rotation, tuning. view = view_proj * proj^-1 (row-
|
||||
// vector); the projection's per-axis signs fold into the rotation so
|
||||
// normals land in the same mirrored space ViewPos reconstructs positions
|
||||
// in; reflection directions are mirror-SENSITIVE, unlike every AO term.
|
||||
const float* pr = scene.proj;
|
||||
float c[32] = {};
|
||||
c[0] = float(g_r.ssr_width);
|
||||
c[1] = float(g_r.ssr_height);
|
||||
c[2] = 1.0f / float(g_r.ssr_width);
|
||||
c[3] = 1.0f / float(g_r.ssr_height);
|
||||
c[4] = std::fabs(pr[0]);
|
||||
c[5] = std::fabs(pr[5]);
|
||||
c[6] = pr[10];
|
||||
c[7] = pr[14];
|
||||
const float sxs = pr[0] < 0.0f ? -1.0f : 1.0f;
|
||||
const float sys = pr[5] < 0.0f ? -1.0f : 1.0f;
|
||||
const float inv00 = 1.0f / pr[0];
|
||||
const float inv11 = 1.0f / pr[5];
|
||||
for (int r = 0; r < 3; ++r) {
|
||||
// proj^-1 columns: x/y scale by 1/m00 / 1/m11, view z = clip w (the
|
||||
// view_proj row's w column); the AO-space axis signs multiply in.
|
||||
c[8 + r * 4 + 0] = scene.view_proj[r * 4 + 0] * inv00 * sxs;
|
||||
c[8 + r * 4 + 1] = scene.view_proj[r * 4 + 1] * inv11 * sys;
|
||||
c[8 + r * 4 + 2] = scene.view_proj[r * 4 + 3];
|
||||
}
|
||||
c[20] = float(REXCVAR_GET(skate3_native_render_scene_ssr_steps));
|
||||
c[21] = float(REXCVAR_GET(skate3_native_render_scene_ssr_thickness));
|
||||
c[22] = float(REXCVAR_GET(skate3_native_render_scene_ssr_intensity));
|
||||
c[23] = float(REXCVAR_GET(skate3_native_render_scene_ssr_debug));
|
||||
c[24] = 250.0f; // max ray distance (view units; the screen clip governs)
|
||||
|
||||
cmd->SetBindingLayout(g_r.ssr_layout);
|
||||
cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList);
|
||||
cmd->SetRootConstants(0, 32, c, 0);
|
||||
|
||||
// 1) Own linearize when AO did not produce the plane this frame.
|
||||
if (!lin_depth_ready) {
|
||||
cmd->Barrier(g_r.depth, nrhi::ResourceState::kDepthWrite,
|
||||
nrhi::ResourceState::kPixelShaderResource);
|
||||
cmd->FlushBarriers();
|
||||
cmd->SetViewport(viewport);
|
||||
cmd->SetScissor(scissor);
|
||||
cmd->SetRenderTargets(g_r.ssr_lin_depth, nullptr);
|
||||
cmd->SetPipeline(g_r.pso_ssr_linearize);
|
||||
cmd->SetTexture(1, g_r.ssr_depth_srv);
|
||||
cmd->Draw(3, 0);
|
||||
cmd->Barrier(g_r.depth, nrhi::ResourceState::kPixelShaderResource,
|
||||
nrhi::ResourceState::kDepthWrite);
|
||||
}
|
||||
cmd->Barrier(lin_tex, nrhi::ResourceState::kRenderTarget,
|
||||
nrhi::ResourceState::kPixelShaderResource);
|
||||
|
||||
// 2) March (half res): the HDR plane pauses as a sampled source.
|
||||
cmd->Barrier(g_r.hdr_resolved, nrhi::ResourceState::kRenderTarget,
|
||||
nrhi::ResourceState::kPixelShaderResource);
|
||||
cmd->FlushBarriers();
|
||||
const nrhi::Viewport ssr_vp{0.0f, 0.0f, float(g_r.ssr_width),
|
||||
float(g_r.ssr_height), 0.0f, 1.0f};
|
||||
const nrhi::Rect ssr_sc{0, 0, int32_t(g_r.ssr_width),
|
||||
int32_t(g_r.ssr_height)};
|
||||
cmd->SetViewport(ssr_vp);
|
||||
cmd->SetScissor(ssr_sc);
|
||||
cmd->SetRenderTargets(g_r.ssr_tex, nullptr);
|
||||
cmd->SetPipeline(g_r.pso_ssr_march);
|
||||
cmd->SetTexture(1, g_r.ssr_gbuf_srv);
|
||||
cmd->SetTexture(2, lin_srv);
|
||||
cmd->SetTexture(3, g_r.hdr_srv);
|
||||
cmd->Draw(3, 0);
|
||||
cmd->Barrier(g_r.ssr_tex, nrhi::ResourceState::kRenderTarget,
|
||||
nrhi::ResourceState::kPixelShaderResource);
|
||||
cmd->Barrier(g_r.hdr_resolved, nrhi::ResourceState::kPixelShaderResource,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
cmd->FlushBarriers();
|
||||
|
||||
// 3) Composite (full res, src-alpha blend onto the HDR plane). Dest-size
|
||||
// constants switch to the full raster; the rest carries over.
|
||||
c[0] = float(width);
|
||||
c[1] = float(height);
|
||||
c[2] = 1.0f / float(width);
|
||||
c[3] = 1.0f / float(height);
|
||||
cmd->SetRootConstants(0, 4, c, 0);
|
||||
cmd->SetViewport(viewport);
|
||||
cmd->SetScissor(scissor);
|
||||
cmd->SetRenderTargets(g_r.hdr_resolved, nullptr);
|
||||
cmd->SetPipeline(g_r.pso_ssr_composite);
|
||||
cmd->SetTexture(1, g_r.ssr_srv);
|
||||
cmd->SetTexture(2, g_r.ssr_gbuf_srv);
|
||||
cmd->SetTexture(3, lin_srv);
|
||||
cmd->Draw(3, 0);
|
||||
|
||||
// Restore intermediate steady states.
|
||||
cmd->Barrier(g_r.ssr_tex, nrhi::ResourceState::kPixelShaderResource,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
cmd->Barrier(lin_tex, nrhi::ResourceState::kPixelShaderResource,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
cmd->Barrier(g_r.ssr_gbuf, nrhi::ResourceState::kPixelShaderResource,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
cmd->FlushBarriers();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- HDR post chain (hdr.hlsl: bloom pyramid + the extracted tonemap) ----
|
||||
// Layout + PSOs, built when the HDR path activates and rebuilt when the
|
||||
// guest output format (the tonemap target) changes. Failure falls back to
|
||||
|
|
@ -17801,6 +18276,28 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
return nullptr;
|
||||
};
|
||||
|
||||
// ---- SSR reflective-item capture (see ApplySsrPass) ----
|
||||
// draw_item records each reflective item's fully-staged draw state
|
||||
// (constants + resolved texture views) so the reflection G-buffer pass
|
||||
// after the scene pass re-renders exactly what the main pass drew.
|
||||
struct SsrGbufItem {
|
||||
float constants[52];
|
||||
nrhi::TextureView* t3; // macro slot (water ripple normal map)
|
||||
nrhi::TextureView* t4; // spec/reflection masks
|
||||
nrhi::TextureView* t5; // fam 5/6 normal map (white when unpaired)
|
||||
uint32_t mesh;
|
||||
uint64_t fingerprint;
|
||||
const DrawItem* item;
|
||||
};
|
||||
std::vector<SsrGbufItem> ssr_items;
|
||||
const bool ssr_on =
|
||||
hdr_on && use_depth && debug_mode == 0 && !loading_native &&
|
||||
!g_r.ssr_failed && REXCVAR_GET(skate3_native_render_scene_ssr) &&
|
||||
// The published projection must be the live perspective matrix (same
|
||||
// gate as the SSAO pass).
|
||||
scene.proj[11] == 1.0f && scene.proj[0] != 0.0f &&
|
||||
scene.proj[5] != 0.0f && scene.proj[14] != 0.0f;
|
||||
|
||||
const auto draw_item = [&](const DrawItem& item) {
|
||||
// NO per-frame inline decodes here. Static content (world geometry,
|
||||
// props) loads/heals on the decode workers via the miss queue; a
|
||||
|
|
@ -18786,6 +19283,24 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
constants[47] = 0.0f;
|
||||
constants[49] = scene.sky_sun[5]; // misc.y = scene exposure
|
||||
}
|
||||
// SSR: record reflective items, env fams 5/6/13 on their exact branch
|
||||
// with live spec masks (the reflection mask rides t4.z), plus water,
|
||||
// for the reflection G-buffer pass after the scene pass.
|
||||
if (ssr_on && !item.skinned &&
|
||||
(item.water ||
|
||||
((item.env_family == 5 || item.env_family == 6 ||
|
||||
item.env_family == 13) &&
|
||||
constants[39] < -4.5f && constants[47] > 2.5f))) {
|
||||
SsrGbufItem si;
|
||||
std::memcpy(si.constants, constants, sizeof(constants));
|
||||
si.t3 = macro_tex->srv;
|
||||
si.t4 = decal_tex->srv;
|
||||
si.t5 = normal_paired ? pair_normal->srv : g_r.white.srv;
|
||||
si.mesh = item.mesh;
|
||||
si.fingerprint = item.fingerprint;
|
||||
si.item = &item;
|
||||
ssr_items.push_back(si);
|
||||
}
|
||||
cmd->SetRootConstants(0, 52, constants, 0);
|
||||
|
||||
cmd->SetTexture(1, diffuse->srv);
|
||||
|
|
@ -19444,6 +19959,74 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
RenderOutlineMask(context, scene, viewport, scissor, msaa_on, scene_color,
|
||||
g_r.depth, use_depth);
|
||||
|
||||
// ---- SSR reflection G-buffer (scene.hlsl ps_refl_gbuf) ----
|
||||
// Re-render the frame's reflective items (captured with their main-pass
|
||||
// constants/textures by draw_item) into the half-res G-buffer while the
|
||||
// main binding layout is still live. The march/composite consume it after
|
||||
// the resolve + AO passes (ApplySsrPass); it hands off in
|
||||
// PIXEL_SHADER_RESOURCE state via ssr_gbuf_ready.
|
||||
g_r.ssr_gbuf_ready = false;
|
||||
if (!ssr_items.empty() && EnsureSsrPipeline(context) &&
|
||||
EnsureSsrTargets(context)) {
|
||||
// Front-to-back for early-z; the pass's own depth buffer resolves
|
||||
// overlap (a curved facade's far side shares texels with its near side
|
||||
// within one mesh; no draw order can fix that).
|
||||
std::stable_sort(ssr_items.begin(), ssr_items.end(),
|
||||
[&](const SsrGbufItem& a, const SsrGbufItem& b) {
|
||||
return view_dist2(*a.item) < view_dist2(*b.item);
|
||||
});
|
||||
const float gbuf_clear[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
cmd->ClearRenderTarget(g_r.ssr_gbuf, gbuf_clear);
|
||||
cmd->ClearDepth(g_r.ssr_gbuf_depth, 1.0f);
|
||||
cmd->SetRenderTargets(g_r.ssr_gbuf, g_r.ssr_gbuf_depth);
|
||||
cmd->SetViewport(nrhi::Viewport{0.0f, 0.0f, float(g_r.ssr_width),
|
||||
float(g_r.ssr_height), 0.0f, 1.0f});
|
||||
cmd->SetScissor(
|
||||
nrhi::Rect{0, 0, int32_t(g_r.ssr_width), int32_t(g_r.ssr_height)});
|
||||
cmd->SetPipeline(g_r.pso_ssr_gbuf);
|
||||
uint32_t gbuf_draws = 0;
|
||||
for (const SsrGbufItem& si : ssr_items) {
|
||||
auto mit = g_r.meshes.find(si.mesh);
|
||||
if (mit == g_r.meshes.end() ||
|
||||
mit->second.fingerprint != si.fingerprint) {
|
||||
continue; // decode swapped since the main-pass draw; next frame
|
||||
}
|
||||
cmd->SetRootConstants(0, 52, si.constants, 0);
|
||||
cmd->SetTexture(4, si.t3);
|
||||
cmd->SetTexturePair(5, si.t4, si.t5);
|
||||
cmd->SetVertexBuffer(mit->second.vb_view.buffer,
|
||||
mit->second.vb_view.offset,
|
||||
mit->second.vb_view.size_bytes,
|
||||
mit->second.vb_view.stride);
|
||||
cmd->SetIndexBuffer(mit->second.ib_view.buffer,
|
||||
mit->second.ib_view.offset,
|
||||
mit->second.ib_view.size_bytes);
|
||||
for (const DrawEntry& draw : si.item->draws) {
|
||||
if (draw.prim == 4) {
|
||||
cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList);
|
||||
} else if (draw.prim == 6) {
|
||||
cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleStrip);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
cmd->DrawIndexed(draw.index_count, draw.start_index,
|
||||
draw.base_vertex);
|
||||
++gbuf_draws;
|
||||
}
|
||||
}
|
||||
// Restore the pass state the resolve/post paths rely on.
|
||||
cmd->SetViewport(viewport);
|
||||
cmd->SetScissor(scissor);
|
||||
if (!msaa_on) {
|
||||
cmd->SetRenderTargets(scene_color, use_depth ? g_r.depth : nullptr);
|
||||
}
|
||||
if (gbuf_draws > 0) {
|
||||
cmd->Barrier(g_r.ssr_gbuf, nrhi::ResourceState::kRenderTarget,
|
||||
nrhi::ResourceState::kPixelShaderResource);
|
||||
g_r.ssr_gbuf_ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (msaa_on) {
|
||||
// Resolve: average the MSAA samples into the 1x scene plane (the float
|
||||
// HDR plane, or the guest output on the classic path) with a fullscreen
|
||||
|
|
@ -19479,10 +20062,23 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
// overlay, blur). Runs after the outline composite, which relies on the
|
||||
// main layout latched from the scene pass.
|
||||
bool post_ran = false;
|
||||
bool ssao_ran = false;
|
||||
if (use_depth && !loading_native &&
|
||||
REXCVAR_GET(skate3_native_render_scene_ssao) &&
|
||||
ApplySsaoPass(context, cmd, scene, viewport, scissor)) {
|
||||
post_ran = true;
|
||||
ssao_ran = true;
|
||||
}
|
||||
|
||||
// ---- Screen-space reflections (ssr.hlsl) ----
|
||||
// March + composite onto the pre-tonemap HDR plane, between the AO pass
|
||||
// and the HDR post, so reflections tonemap and bloom exactly like
|
||||
// directly-visible scenery. The reflection G-buffer was drawn inside the
|
||||
// scene pass (ssr_gbuf_ready); the SSAO linear-depth plane is reused when
|
||||
// AO ran this frame.
|
||||
if (g_r.ssr_gbuf_ready &&
|
||||
ApplySsrPass(context, cmd, scene, viewport, scissor, ssao_ran)) {
|
||||
post_ran = true;
|
||||
}
|
||||
|
||||
// ---- HDR post (hdr.hlsl: bloom pyramid + the extracted tonemap) ----
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue