render: dynamicobject v2 - per-pixel maps + native static world-shadow
This commit is contained in:
parent
a64c9afb9a
commit
70b4e691bc
4 changed files with 2530 additions and 1897 deletions
|
|
@ -62,6 +62,14 @@ cbuffer S : register(b1) {
|
||||||
// cross(binormal, normal) x handedness to the game's tangent; +-1,
|
// cross(binormal, normal) x handedness to the game's tangent; +-1,
|
||||||
// cvar-tunable against the emulated reference), yzw spare.
|
// cvar-tunable against the emulated reference), yzw spare.
|
||||||
float4 sh_v2;
|
float4 sh_v2;
|
||||||
|
// dynamicobject static world-shadow transform (the game's PS c5/c6/c7):
|
||||||
|
// u = dot(wp4, dyn_wsx) * 0.5 + 0.5, v = dot(wp4, dyn_wsy) * -0.5 + 0.5,
|
||||||
|
// ray depth = dot(wp4, dyn_wsz). Compared against the natively
|
||||||
|
// re-rendered 512x512 baked-shade map at t4 (misc.z flag 8; see the
|
||||||
|
// dynobj branch).
|
||||||
|
float4 dyn_wsx;
|
||||||
|
float4 dyn_wsy;
|
||||||
|
float4 dyn_wsz;
|
||||||
};
|
};
|
||||||
// Character-family lighting (defaultcharacter.fx and friends): canonical
|
// Character-family lighting (defaultcharacter.fx and friends): canonical
|
||||||
// per-draw rows captured from the guest PIXEL constant bank at palette
|
// per-draw rows captured from the guest PIXEL constant bank at palette
|
||||||
|
|
@ -88,10 +96,10 @@ Texture2D<float4> decal_art : register(t4);
|
||||||
Texture2D<float4> normal_map : register(t5);
|
Texture2D<float4> normal_map : register(t5);
|
||||||
Texture2D<float2> shadow_atlas : register(t7);
|
Texture2D<float2> shadow_atlas : register(t7);
|
||||||
TextureCube<float4> env_cube : register(t6);
|
TextureCube<float4> env_cube : register(t6);
|
||||||
// World-shading v2 material maps (the t8/t9 pair table): the environment
|
// World-shading v2 material maps (the t8/t9 pair table): the detail normal
|
||||||
// families' detail normal map (t8, sampled at uv * misc.w) and the decal
|
// map (t8, sampled at uv * misc.w) and the spec/ecc masks (t9). Sampled
|
||||||
// families' spec/ecc masks (t9). Sampled only when the misc.z bind flags
|
// only when the misc.z bind flags say so (env fams 1-4 and the
|
||||||
// say so (fams 1-4; see the env branch).
|
// dynamicobject families; see those branches).
|
||||||
Texture2D<float4> detail_map : register(t8);
|
Texture2D<float4> detail_map : register(t8);
|
||||||
Texture2D<float4> spec2_map : register(t9);
|
Texture2D<float4> spec2_map : register(t9);
|
||||||
// Raw bone palette: 3 float4 rows per bone, column-vector affine [R | t],
|
// Raw bone palette: 3 float4 rows per bone, column-vector affine [R | t],
|
||||||
|
|
@ -231,6 +239,37 @@ float SampleCsmShadow(float3 wp, float extra_bias) {
|
||||||
float2 sm2 = shadow_atlas.Sample(smp_clamp, suv);
|
float2 sm2 = shadow_atlas.Sample(smp_clamp, suv);
|
||||||
return saturate((sm2.x >= rd ? 1.0 : 0.0) + (1.0 - sm2.y));
|
return saturate((sm2.x >= rd ? 1.0 : 0.0) + (1.0 - sm2.y));
|
||||||
}
|
}
|
||||||
|
// Screen-space UV-gradient tangent frame (cotangent reconstruction from the
|
||||||
|
// position/uv derivatives; signs/lengths calibrated against the meshes' REAL
|
||||||
|
// stored TBN; see the env-family branch notes). The RAW screen frame is
|
||||||
|
// exact for the GetTangentLight kd SIGN terms under ANY UV orientation:
|
||||||
|
// bowl/ramp walls map their texture rotated relative to world-up, and
|
||||||
|
// mirrored islands flip dP/dU exactly like the stored tangent does.
|
||||||
|
void ScreenTangentFrame(float3 wn, float3 rpos, float2 uv,
|
||||||
|
out float3 tt, out float3 bb) {
|
||||||
|
float3 dp1 = ddx(rpos), dp2 = ddy(rpos);
|
||||||
|
float2 du1 = ddx(uv), du2 = ddy(uv);
|
||||||
|
float3 dp2p = cross(dp2, wn), dp1p = cross(wn, dp1);
|
||||||
|
tt = dp2p * du1.x + dp1p * du2.x;
|
||||||
|
bb = dp2p * du1.y + dp1p * du2.y;
|
||||||
|
tt *= -rsqrt(max(dot(tt, tt), 1e-12));
|
||||||
|
bb *= rsqrt(max(dot(bb, bb), 1e-12));
|
||||||
|
}
|
||||||
|
// kd/normal-mapping axes: prefer the mesh's STORED frame (binormal +
|
||||||
|
// handedness in tanb, T = cross(B, N) x handedness x calibration polarity)
|
||||||
|
// - authoring decides per UV island whether the frame follows a mirror,
|
||||||
|
// which no derivative frame can know (per-island brightness steps on the
|
||||||
|
// wooden ramp panels / faint bowl striping). The screen frame is the
|
||||||
|
// fallback for meshes without one.
|
||||||
|
void KdAxes(float4 tanb, float3 wn, float3 tt_s, float3 bb_s,
|
||||||
|
out float3 kt, out float3 kb) {
|
||||||
|
kt = tt_s;
|
||||||
|
kb = bb_s;
|
||||||
|
if (tanb.w > 0.1) {
|
||||||
|
kb = normalize(tanb.xyz);
|
||||||
|
kt = cross(kb, wn) * ((tanb.w > 0.6 ? 1.0 : -1.0) * sh_v2.x);
|
||||||
|
}
|
||||||
|
}
|
||||||
float4 ps_main(VSOut i) : SV_Target {
|
float4 ps_main(VSOut i) : SV_Target {
|
||||||
if (tint.a > 0.0) {
|
if (tint.a > 0.0) {
|
||||||
return tint;
|
return tint;
|
||||||
|
|
@ -284,13 +323,17 @@ float4 ps_main(VSOut i) : SV_Target {
|
||||||
}
|
}
|
||||||
// dynamicobject.fx props (cam_pos.w = -(20 + variant): -21 default,
|
// dynamicobject.fx props (cam_pos.w = -(20 + variant): -21 default,
|
||||||
// -22 alphatest). Rigid movable objects (dispensers, dumpsters, benches,
|
// -22 alphatest). Rigid movable objects (dispensers, dumpsters, benches,
|
||||||
// cans). Lit with the game's own dynamicobject model (verified exact):
|
// cans). Lit with the game's own dynamicobject model (verified exact
|
||||||
// key sun light + bounce + flat ambient, gated by
|
// against an offline ucode evaluation): key sun light + bounce +
|
||||||
// the CSM shadow (the static world-shadow map is approximated as fully lit
|
// flat ambient, gated by min(CSM, max(static world shade, c8.w floor)),
|
||||||
// - its floor c8.w bounds it), then fog -> exposure -> tonemap -> sqrt and
|
// then fog -> exposure -> tonemap -> sqrt and the postfx uber 1.41.
|
||||||
// the postfx uber 1.41. v1 uses the geometric normal (cross of the mesh
|
// v2: misc.z
|
||||||
// tangent/binormal) with the flat normal-map kd 0.93429; the base/detail/
|
// carries the material bind flags (1 = base normal at t5, 2 = detail at
|
||||||
// spec normal maps are v2.
|
// t8 sampled at uv * misc.w, 4 = spec/ecc masks at t9: the env fams 1-4
|
||||||
|
// encoding); the mapped world normal drives key/bounce, the real
|
||||||
|
// GetTangentLight kd replaces the flat fold, and the phong spec returns.
|
||||||
|
// With no maps bound the flat-map fold 0.39 * 2.39562 = 0.93429 and the
|
||||||
|
// vertex normal apply (the v1 look).
|
||||||
if (cam_pos.w < -20.5) {
|
if (cam_pos.w < -20.5) {
|
||||||
if (cam_pos.w < -21.5) {
|
if (cam_pos.w < -21.5) {
|
||||||
clip(albedo.a - 0.1176); // dynamicobject.alphatest: ALPHAREF 30
|
clip(albedo.a - 0.1176); // dynamicobject.alphatest: ALPHAREF 30
|
||||||
|
|
@ -299,19 +342,107 @@ float4 ps_main(VSOut i) : SV_Target {
|
||||||
float3 n = dot(i.nrm, i.nrm) > 0.01
|
float3 n = dot(i.nrm, i.nrm) > 0.01
|
||||||
? normalize(i.nrm)
|
? normalize(i.nrm)
|
||||||
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
|
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
|
||||||
float ndl = dot(n, dyn_sun.xyz);
|
uint v2f = (uint)(misc.z + 0.5);
|
||||||
|
// Tangent axes for the kd sign dots / normal mapping / tangent-space
|
||||||
|
// spec mirror: the stored per-vertex frame when the mesh carries one
|
||||||
|
// (packed by DecodeMesh from the usage-6/7 tangent+binormal pair),
|
||||||
|
// screen-space UV-gradient frame otherwise.
|
||||||
|
float3 kt = float3(1.0, 0.0, 0.0), kb = float3(0.0, 1.0, 0.0);
|
||||||
|
if (v2f != 0u) {
|
||||||
|
float3 tt_s, bb_s;
|
||||||
|
ScreenTangentFrame(n, i.rpos, i.uv, tt_s, bb_s);
|
||||||
|
KdAxes(i.tanb, n, tt_s, bb_s, kt, kb);
|
||||||
|
}
|
||||||
|
// GetTangentLight's fixed SIGNED tangent-space light: the signs come
|
||||||
|
// from the frame axes dotted with the dynobj bank's own sun (c9:
|
||||||
|
// dyn_sun, not the world families' sh_sun).
|
||||||
|
float3 Lt = float3(0.58 * sign(dot(kt, dyn_sun.xyz)),
|
||||||
|
0.62 * sign(dot(kb, dyn_sun.xyz)), 0.39);
|
||||||
|
// vnd = (2*base.xy + 2*detail.xy - 2, 2*base.z - 1), NORMALIZED for
|
||||||
|
// both the kd dot and the TBN mapping (both dynobj variants).
|
||||||
|
float3 vndn = float3(0.0, 0.0, 1.0);
|
||||||
|
float3 wn = n;
|
||||||
|
float kd = 0.93429;
|
||||||
|
if ((v2f & 1u) != 0u) {
|
||||||
|
float3 nmv = normal_map.Sample(smp, i.uv).rgb;
|
||||||
|
float2 dxy = (v2f & 2u) != 0u
|
||||||
|
? detail_map.Sample(smp, i.uv * misc.w).rg
|
||||||
|
: float2(0.5, 0.5);
|
||||||
|
vndn = normalize(
|
||||||
|
float3(nmv.xy * 2.0 + dxy * 2.0 - 2.0, nmv.z * 2.0 - 1.0));
|
||||||
|
wn = normalize(vndn.x * kt + vndn.y * kb + vndn.z * n);
|
||||||
|
kd = dot(vndn, Lt) * 2.39562;
|
||||||
|
}
|
||||||
|
float ndl = dot(wn, dyn_sun.xyz);
|
||||||
float key = saturate(ndl); // key light gated on N.L >= 0
|
float key = saturate(ndl); // key light gated on N.L >= 0
|
||||||
float bounce = saturate(dot(n, float3(-dyn_sun.x, dyn_sun.y, -dyn_sun.z)));
|
float bounce = saturate(dot(wn, float3(-dyn_sun.x, dyn_sun.y, -dyn_sun.z)));
|
||||||
// CSM shadow (shared receiver rows at t5); the static world-shadow map is
|
// CSM shadow (shared receiver rows at t7). extra_bias -1 = the game's
|
||||||
// not rendered natively, so its term is 1 and the min collapses to the
|
// own per-cascade dynamicobject receive bias (props are casters;
|
||||||
// CSM (bounded below by the c8.w floor, dyn_misc.y). extra_bias -1 =
|
// without it their flat tops self-shadow and flicker).
|
||||||
// the game's own per-cascade dynamicobject receive bias (props are
|
|
||||||
// casters; without it their flat tops self-shadow and flicker).
|
|
||||||
float s = SampleCsmShadow(i.rpos + cam_pos.xyz, -1.0);
|
float s = SampleCsmShadow(i.rpos + cam_pos.xyz, -1.0);
|
||||||
float shadow = min(s * (ndl >= 0.0 ? 1.0 : 0.0), max(1.0, dyn_misc.y));
|
// Static world-shadow term: the game's baked building/tree shade,
|
||||||
|
// sampled from the natively re-rendered map at t4 (flag 8) with the
|
||||||
|
// PS's own 4-tap PCF (point taps at +-0.5 texels, manual bilinear:
|
||||||
|
// tap.x >= ray depth reads lit; uncovered texels hold the far clear).
|
||||||
|
// Without the map the term is 1 and the min collapses to the CSM:
|
||||||
|
// props standing in baked shade lit at full key (the newspaper-machine
|
||||||
|
// mint brightening).
|
||||||
|
float world = 1.0;
|
||||||
|
if ((v2f & 8u) != 0u) {
|
||||||
|
float4 wp4 = float4(i.rpos + cam_pos.xyz, 1.0);
|
||||||
|
float2 wuv = float2(dot(wp4, dyn_wsx) * 0.5 + 0.5,
|
||||||
|
dot(wp4, dyn_wsy) * -0.5 + 0.5);
|
||||||
|
float rdw = dot(wp4, dyn_wsz);
|
||||||
|
float2 st = wuv * 512.0 - 0.5;
|
||||||
|
float2 fw = frac(st);
|
||||||
|
int2 b0 = int2(floor(st));
|
||||||
|
float4 t;
|
||||||
|
t.x = decal_art.Load(int3(clamp(b0, int2(0, 0), int2(511, 511)), 0)).x >=
|
||||||
|
rdw
|
||||||
|
? 1.0
|
||||||
|
: 0.0;
|
||||||
|
t.y = decal_art.Load(int3(clamp(b0 + int2(1, 0), int2(0, 0),
|
||||||
|
int2(511, 511)),
|
||||||
|
0)).x >= rdw
|
||||||
|
? 1.0
|
||||||
|
: 0.0;
|
||||||
|
t.z = decal_art.Load(int3(clamp(b0 + int2(0, 1), int2(0, 0),
|
||||||
|
int2(511, 511)),
|
||||||
|
0)).x >= rdw
|
||||||
|
? 1.0
|
||||||
|
: 0.0;
|
||||||
|
t.w = decal_art.Load(int3(clamp(b0 + int2(1, 1), int2(0, 0),
|
||||||
|
int2(511, 511)),
|
||||||
|
0)).x >= rdw
|
||||||
|
? 1.0
|
||||||
|
: 0.0;
|
||||||
|
world = lerp(lerp(t.x, t.y, fw.x), lerp(t.z, t.w, fw.x), fw.y);
|
||||||
|
}
|
||||||
|
float shadow = min(s * (ndl >= 0.0 ? 1.0 : 0.0), max(world, dyn_misc.y));
|
||||||
float3 lighting = key * shadow + bounce * dyn_amb.w + dyn_amb.rgb;
|
float3 lighting = key * shadow + bounce * dyn_amb.w + dyn_amb.rgb;
|
||||||
// GetTangentLight with the neutral (flat) normal map: 0.39 * 2.39562.
|
float3 lin = lighting * kd * dlin;
|
||||||
float3 lin = lighting * 0.93429 * dlin;
|
// Phong spec vs the material's spec mask (t9.x) / eccentricity (t9.y),
|
||||||
|
// x shadow, tint (2.1, 1.8, 1.5). Two variants: the default PS
|
||||||
|
// reflects the fixed SIGNED tangent-space light about vnd with the eye
|
||||||
|
// transformed into tangent space (it shares the kd sign dots); the
|
||||||
|
// alphatest PS reflects the fixed WORLD light (-0.14, 0.5, 0.9) about
|
||||||
|
// wN exactly like baseenvironment.
|
||||||
|
if ((v2f & 4u) != 0u) {
|
||||||
|
float2 m2 = spec2_map.Sample(smp, i.uv).rg;
|
||||||
|
float3 E = -normalize(i.rpos);
|
||||||
|
float bp;
|
||||||
|
if (cam_pos.w < -21.5) {
|
||||||
|
float3 Ls = float3(-0.14, 0.5, 0.9);
|
||||||
|
float3 refl = Ls - 2.0 * wn * dot(wn, Ls);
|
||||||
|
bp = saturate(dot(E, -refl));
|
||||||
|
} else {
|
||||||
|
float3 Et = normalize(float3(dot(E, kt), dot(E, kb), dot(E, n)));
|
||||||
|
float3 refl = Lt - 2.0 * dot(vndn, Lt) * vndn;
|
||||||
|
bp = saturate(dot(Et, -refl));
|
||||||
|
}
|
||||||
|
float ks = bp > 0.0 ? pow(max(bp, 1e-6), 10.0 + 290.0 * m2.y) : 0.0;
|
||||||
|
lin += ks * float3(2.1, 1.8, 1.5) * (shadow * m2.x);
|
||||||
|
}
|
||||||
// Fog -> exposure -> tonemap -> sqrt, then the 1.41 uber scene multiplier.
|
// Fog -> exposure -> tonemap -> sqrt, then the 1.41 uber scene multiplier.
|
||||||
float fdist = length(i.rpos);
|
float fdist = length(i.rpos);
|
||||||
float f1 = saturate(fdist * sh_fogp.x + sh_fogp.y);
|
float f1 = saturate(fdist * sh_fogp.x + sh_fogp.y);
|
||||||
|
|
@ -611,10 +742,9 @@ float4 ps_main(VSOut i) : SV_Target {
|
||||||
}
|
}
|
||||||
// Tangent frames shared by kd and the reflective normal mapping.
|
// Tangent frames shared by kd and the reflective normal mapping.
|
||||||
// Two variants of the same construction:
|
// Two variants of the same construction:
|
||||||
// - tt_s/bb_s = the RAW screen-space UV-gradient frame. Exact for
|
// - tt_s/bb_s = the RAW screen-space UV-gradient frame
|
||||||
// the kd SIGN terms under ANY UV orientation; bowl/ramp walls
|
// (ScreenTangentFrame: exact for the kd sign terms under any UV
|
||||||
// map their texture rotated relative to world-up, and mirrored
|
// orientation).
|
||||||
// islands flip dP/dU exactly like the stored tangent does.
|
|
||||||
// - tt/bb = analytic world-up axes carrying the screen frame's
|
// - tt/bb = analytic world-up axes carrying the screen frame's
|
||||||
// signs; degree-accurate where the mapping IS world-up aligned
|
// signs; degree-accurate where the mapping IS world-up aligned
|
||||||
// (building facades, calibrated for the glass reflections).
|
// (building facades, calibrated for the glass reflections).
|
||||||
|
|
@ -626,13 +756,7 @@ float4 ps_main(VSOut i) : SV_Target {
|
||||||
float3 tt = float3(1.0, 0.0, 0.0), bb = float3(0.0, 1.0, 0.0);
|
float3 tt = float3(1.0, 0.0, 0.0), bb = float3(0.0, 1.0, 0.0);
|
||||||
float3 tt_s = tt, bb_s = bb;
|
float3 tt_s = tt, bb_s = bb;
|
||||||
if (have_vnd || (overlay.w > 3.5 && abs(misc.z - 3.0) > 0.5)) {
|
if (have_vnd || (overlay.w > 3.5 && abs(misc.z - 3.0) > 0.5)) {
|
||||||
float3 dp1 = ddx(i.rpos), dp2 = ddy(i.rpos);
|
ScreenTangentFrame(wn, i.rpos, i.uv, tt, bb);
|
||||||
float2 du1 = ddx(i.uv), du2 = ddy(i.uv);
|
|
||||||
float3 dp2p = cross(dp2, wn), dp1p = cross(wn, dp1);
|
|
||||||
tt = dp2p * du1.x + dp1p * du2.x;
|
|
||||||
bb = dp2p * du1.y + dp1p * du2.y;
|
|
||||||
tt *= -rsqrt(max(dot(tt, tt), 1e-12));
|
|
||||||
bb *= rsqrt(max(dot(bb, bb), 1e-12));
|
|
||||||
tt_s = tt;
|
tt_s = tt;
|
||||||
bb_s = bb;
|
bb_s = bb;
|
||||||
float3 bb2 = float3(0.0, 1.0, 0.0) - wn * wn.y;
|
float3 bb2 = float3(0.0, 1.0, 0.0) - wn * wn.y;
|
||||||
|
|
@ -644,18 +768,9 @@ float4 ps_main(VSOut i) : SV_Target {
|
||||||
bb = bb2 * (dot(bb2, bb) >= 0.0 ? 1.0 : -1.0);
|
bb = bb2 * (dot(bb2, bb) >= 0.0 ? 1.0 : -1.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// kd axes: prefer the mesh's STORED frame (binormal + handedness in
|
// kd axes: stored per-vertex frame with the screen fallback (KdAxes).
|
||||||
// i.tanb, T = cross(B, N) x handedness x calibration polarity);
|
float3 kt, kb;
|
||||||
// authoring decides per UV island whether the frame follows a
|
KdAxes(i.tanb, wn, tt_s, bb_s, kt, kb);
|
||||||
// mirror, which no derivative frame can know (per-island brightness
|
|
||||||
// steps on the wooden ramp panels / faint bowl striping). Screen
|
|
||||||
// frame is the fallback for meshes without one.
|
|
||||||
float3 kt = tt_s, kb = bb_s;
|
|
||||||
if (i.tanb.w > 0.1) {
|
|
||||||
kb = normalize(i.tanb.xyz);
|
|
||||||
kt = cross(kb, wn) *
|
|
||||||
((i.tanb.w > 0.6 ? 1.0 : -1.0) * sh_v2.x);
|
|
||||||
}
|
|
||||||
if (have_vnd) {
|
if (have_vnd) {
|
||||||
// The mapped world normal feeds the spec mirror below; matte spec
|
// The mapped world normal feeds the spec mirror below; matte spec
|
||||||
// is broad-lobed, so exact axis precision matters less than the
|
// is broad-lobed, so exact axis precision matters less than the
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -95,6 +95,13 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_world_v2, true, "Skate 3",
|
||||||
"terms; v1 folded them to the flat-map constants). Off = the v1 "
|
"terms; v1 folded them to the flat-map constants). Off = the v1 "
|
||||||
"flat response, for A/B comparison.")
|
"flat response, for A/B comparison.")
|
||||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_dynobj_v2, true, "Skate 3",
|
||||||
|
"Dynamicobject shading v2: per-pixel normal/detail/spec maps on the "
|
||||||
|
"exact dynamicobject families (mapped world normal, the game's "
|
||||||
|
"GetTangentLight kd and both phong spec variants; v1 folded them "
|
||||||
|
"to the flat-map constants). Off = the v1 flat response, for A/B "
|
||||||
|
"comparison.")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_ssao, true, "Skate 3",
|
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_ssao, true, "Skate 3",
|
||||||
"Screen-space ambient occlusion (GTAO) in the native renderer: soft "
|
"Screen-space ambient occlusion (GTAO) in the native renderer: soft "
|
||||||
"contact shading where surfaces meet (under ledges, rails, vehicles, "
|
"contact shading where surfaces meet (under ledges, rails, vehicles, "
|
||||||
|
|
@ -1083,6 +1090,10 @@ bool g_proxy_frame_done = false;
|
||||||
float g_dynobj_rows[10] = {};
|
float g_dynobj_rows[10] = {};
|
||||||
bool g_dynobj_have = false;
|
bool g_dynobj_have = false;
|
||||||
bool g_dynobj_frame_done = false;
|
bool g_dynobj_frame_done = false;
|
||||||
|
// dynamicobject static world-shadow transform rows (PS c5/c6/c7), captured
|
||||||
|
// alongside g_dynobj_rows, see FrameScene::dynobj_ws.
|
||||||
|
float g_dynobj_ws[12] = {};
|
||||||
|
bool g_dynobj_ws_have = false;
|
||||||
// Dynamic entities (characters, movable props) live entirely in transient
|
// Dynamic entities (characters, movable props) live entirely in transient
|
||||||
// per-frame arenas: the context, its record, mesh pointers, transforms and
|
// per-frame arenas: the context, its record, mesh pointers, transforms and
|
||||||
// bone palettes are all recycled before frame end. The complete DrawItem is
|
// bone palettes are all recycled before frame end. The complete DrawItem is
|
||||||
|
|
@ -4378,28 +4389,66 @@ bool CaptureSkinnedState(uint8_t* base, uint32_t bank, uint32_t palette_base,
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Dropped-garment gate: a garment the game removed (numCloth back to
|
||||||
|
// 0, VB objects freed) must never publish rigid; whatever this bank
|
||||||
|
// holds is foreign (the game draws the ctx skinned now), and a rigid
|
||||||
|
// publish would pair a live world with our retained stale drape.
|
||||||
|
if (skate3::native_entity::RopaGarmentDropped(base, item.ctx,
|
||||||
|
item.vb_obj)) {
|
||||||
|
g_ropa_stale.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// The garment's rigid world is the accepted draw-time bank matrix
|
// The garment's rigid world is the accepted draw-time bank matrix
|
||||||
// (c188/c191): the game stages it tick-exact with the deformed VB
|
// (c188/c191): the game stages it tick-exact with the deformed VB
|
||||||
// content and with the body palettes packed at EndJobs. (A guest-side
|
// content and with the body palettes packed at EndJobs. (A guest-side
|
||||||
// entity L2W read at StartJobs predates the tick's locomotion update;
|
// entity L2W read at StartJobs predates the tick's locomotion update;
|
||||||
// serving that as the draw world rendered the whole shirt one guest
|
// serving that as the draw world rendered the whole shirt one guest
|
||||||
// tick behind the body, a constant velocity-proportional drape lag.)
|
// tick behind the body, a constant velocity-proportional drape lag.)
|
||||||
const uint32_t m = main_pass ? 191u : 188u;
|
uint32_t m = main_pass ? 191u : 188u;
|
||||||
float rows[12];
|
float rows[12];
|
||||||
bool plausible = true;
|
const auto read_rows = [&](uint32_t reg) -> bool {
|
||||||
for (int r = 0; r < 3 && plausible; ++r) {
|
bool ok = true;
|
||||||
float n = 0.0f;
|
for (int r = 0; r < 3 && ok; ++r) {
|
||||||
for (int i = 0; i < 4; ++i) {
|
float n = 0.0f;
|
||||||
const float f = LoadGuestF32(base, bank + ((m + r) * 4 + i) * 4);
|
for (int i = 0; i < 4; ++i) {
|
||||||
if (!(f > -1e7f && f < 1e7f)) {
|
const float f = LoadGuestF32(base, bank + ((reg + r) * 4 + i) * 4);
|
||||||
plausible = false;
|
if (!(f > -1e7f && f < 1e7f)) {
|
||||||
break;
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
rows[r * 4 + i] = f;
|
||||||
|
if (i < 3) n += f * f;
|
||||||
}
|
}
|
||||||
rows[r * 4 + i] = f;
|
ok = ok && n > 0.0025f && n < 400.0f && rows[r * 4 + 3] > -20000.f &&
|
||||||
if (i < 3) n += f * f;
|
rows[r * 4 + 3] < 20000.f;
|
||||||
}
|
}
|
||||||
plausible = plausible && n > 0.0025f && n < 400.0f &&
|
return ok;
|
||||||
rows[r * 4 + 3] > -20000.f && rows[r * 4 + 3] < 20000.f;
|
};
|
||||||
|
const auto tail_row_at = [&](uint32_t reg) -> bool {
|
||||||
|
const float x = LoadGuestF32(base, bank + (reg * 4 + 0) * 4);
|
||||||
|
const float y = LoadGuestF32(base, bank + (reg * 4 + 1) * 4);
|
||||||
|
const float z = LoadGuestF32(base, bank + (reg * 4 + 2) * 4);
|
||||||
|
const float w = LoadGuestF32(base, bank + (reg * 4 + 3) * 4);
|
||||||
|
return std::fabs(x) <= 1e-4f && std::fabs(y) <= 1e-4f &&
|
||||||
|
std::fabs(z) <= 1e-4f && std::fabs(w - 1.0f) <= 1e-4f;
|
||||||
|
};
|
||||||
|
// The passes stage the matrix as a 4-row block each: main at
|
||||||
|
// c191..c194, shadow at c188..c191 (3 affine rows + a (0,0,0,1)
|
||||||
|
// homogeneous tail). The tail at m+3 is REQUIRED: it is the
|
||||||
|
// structural proof a matrix block lives at m. Without it the read is
|
||||||
|
// mid-palette; an 84-bone body bank's palette (c5/c8 onward) covers
|
||||||
|
// c188..c194 with bone matrices, and a bone matrix (world *
|
||||||
|
// inverse-bind) is a plausible, high-scoring affine that renders the
|
||||||
|
// garment rotated by the bind rotation and offset meters from the
|
||||||
|
// body while tracking the skater.
|
||||||
|
bool plausible = read_rows(m) && tail_row_at(m + 3);
|
||||||
|
// The shadow block's tail lands exactly on c191 = the main block's
|
||||||
|
// first row. A main-pass read that finds that tail at c191 is seeing
|
||||||
|
// the post-shadow bank state, where c188..c190 still hold the same
|
||||||
|
// tick's matrix: consume that copy instead of refusing the capture.
|
||||||
|
if (!plausible && main_pass && tail_row_at(191u) && read_rows(188u)) {
|
||||||
|
m = 188u;
|
||||||
|
plausible = true;
|
||||||
}
|
}
|
||||||
bool front_all = false;
|
bool front_all = false;
|
||||||
const int rigid_score =
|
const int rigid_score =
|
||||||
|
|
@ -5097,6 +5146,26 @@ void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t
|
||||||
g_dynobj_rows[7] = LoadGuestF32(base, ps_bank + (15 * 4 + 3) * 4);
|
g_dynobj_rows[7] = LoadGuestF32(base, ps_bank + (15 * 4 + 3) * 4);
|
||||||
g_dynobj_rows[8] = mult;
|
g_dynobj_rows[8] = mult;
|
||||||
g_dynobj_rows[9] = LoadGuestF32(base, ps_bank + (8 * 4 + 3) * 4);
|
g_dynobj_rows[9] = LoadGuestF32(base, ps_bank + (8 * 4 + 3) * 4);
|
||||||
|
// Static world-shadow transform (c5 = u row, c6 = v row,
|
||||||
|
// c7 = ray depth row): consumed by the native world-shadow
|
||||||
|
// re-render + the PS world term. Region-stable rows; a change
|
||||||
|
// re-primes the native map (see RenderShadowAtlas).
|
||||||
|
float ws[12];
|
||||||
|
bool ws_ok = true;
|
||||||
|
float uvmag = 0.0f;
|
||||||
|
for (int k = 0; k < 12; ++k) {
|
||||||
|
ws[k] = LoadGuestF32(base, ps_bank + ((5 + k / 4) * 4 + k % 4) * 4);
|
||||||
|
if (!(ws[k] == ws[k]) || std::fabs(ws[k]) > 1e6f) {
|
||||||
|
ws_ok = false;
|
||||||
|
}
|
||||||
|
if (k < 8 && (k % 4) < 3) {
|
||||||
|
uvmag += std::fabs(ws[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ws_ok && uvmag > 1e-6f) {
|
||||||
|
std::memcpy(g_dynobj_ws, ws, sizeof(ws));
|
||||||
|
g_dynobj_ws_have = true;
|
||||||
|
}
|
||||||
if (!g_dynobj_have) {
|
if (!g_dynobj_have) {
|
||||||
REXLOG_INFO(
|
REXLOG_INFO(
|
||||||
"native-scene: dynamicobject rows captured sun=({:.3f},{:.3f},"
|
"native-scene: dynamicobject rows captured sun=({:.3f},{:.3f},"
|
||||||
|
|
@ -9304,6 +9373,10 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
if (g_dynobj_have) {
|
if (g_dynobj_have) {
|
||||||
std::memcpy(scene.dynobj_rows, g_dynobj_rows, sizeof(g_dynobj_rows));
|
std::memcpy(scene.dynobj_rows, g_dynobj_rows, sizeof(g_dynobj_rows));
|
||||||
scene.dynobj_valid = true;
|
scene.dynobj_valid = true;
|
||||||
|
if (g_dynobj_ws_have) {
|
||||||
|
std::memcpy(scene.dynobj_ws, g_dynobj_ws, sizeof(g_dynobj_ws));
|
||||||
|
scene.dynobj_ws_valid = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (g_sky_have) {
|
if (g_sky_have) {
|
||||||
scene.sky_height = g_sky_height;
|
scene.sky_height = g_sky_height;
|
||||||
|
|
@ -10324,10 +10397,26 @@ struct RendererState {
|
||||||
uint32_t shadow_dump_enqueued = 0;
|
uint32_t shadow_dump_enqueued = 0;
|
||||||
uint32_t shadow_dump_written = 0;
|
uint32_t shadow_dump_written = 0;
|
||||||
bool shadow_dump_done = false;
|
bool shadow_dump_done = false;
|
||||||
// Per-frame shadow constant buffer ring (CBV b1).
|
// Per-frame shadow constant buffer ring (CBV b1). 512-byte slices: the
|
||||||
|
// first 256 bytes are the original 16-row block, rows 16-18 carry the
|
||||||
|
// dynamicobject world-shadow transform (dyn_ws* in the scene shader).
|
||||||
static constexpr uint32_t kShadowCbRegions = 8;
|
static constexpr uint32_t kShadowCbRegions = 8;
|
||||||
|
static constexpr uint32_t kShadowCbSlice = 512;
|
||||||
nrhi::Buffer* shadow_cb = nullptr;
|
nrhi::Buffer* shadow_cb = nullptr;
|
||||||
uint8_t* shadow_cb_cpu = nullptr;
|
uint8_t* shadow_cb_cpu = nullptr;
|
||||||
|
// dynamicobject static world-shadow map (512x512, same convention as the
|
||||||
|
// game's own: x = light-space depth, sge(x >= ray) = lit): re-rendered
|
||||||
|
// natively from the frame's STATIC world items with the captured c5/c6/c7
|
||||||
|
// transform. MIN blend accumulates across frames WITHOUT clearing; the
|
||||||
|
// per-frame item list is view-culled, and dropping off-screen casters
|
||||||
|
// would flicker props' shade with the camera. Cleared (re-primed) only
|
||||||
|
// when the captured transform changes (streaming region switch).
|
||||||
|
nrhi::Texture* world_shadow = nullptr;
|
||||||
|
nrhi::TextureView* world_shadow_srv = nullptr;
|
||||||
|
bool world_shadow_in_srv = false;
|
||||||
|
bool world_shadow_primed = false;
|
||||||
|
float world_shadow_rows[12] = {};
|
||||||
|
static constexpr uint32_t kWorldShadowSize = 512;
|
||||||
static constexpr uint32_t kUiRegionSize = 1u << 20;
|
static constexpr uint32_t kUiRegionSize = 1u << 20;
|
||||||
static constexpr uint32_t kUiRegions = 8;
|
static constexpr uint32_t kUiRegions = 8;
|
||||||
nrhi::Buffer* ui_ring = nullptr;
|
nrhi::Buffer* ui_ring = nullptr;
|
||||||
|
|
@ -10818,6 +10907,11 @@ bool DecodeMesh(nrhi::Device* device, uint8_t* base, const DrawItem& item,
|
||||||
out[1] = float(iy) / 1023.0f;
|
out[1] = float(iy) / 1023.0f;
|
||||||
out[2] = float(iz) / 511.0f;
|
out[2] = float(iz) / 511.0f;
|
||||||
};
|
};
|
||||||
|
// Stored-tangent-frame pack (world-shading / dynobj v2): snorm
|
||||||
|
// component to the unorm byte of the blend-weight word.
|
||||||
|
const auto pk = [](float f) {
|
||||||
|
return uint32_t(std::lround((f * 0.5f + 0.5f) * 255.0f)) & 0xFFu;
|
||||||
|
};
|
||||||
float n3[3] = {0.0f, 0.0f, 0.0f};
|
float n3[3] = {0.0f, 0.0f, 0.0f};
|
||||||
if (item.env_family != 0 &&
|
if (item.env_family != 0 &&
|
||||||
(item.env_family <= 6 || item.env_family == 13) &&
|
(item.env_family <= 6 || item.env_family == 13) &&
|
||||||
|
|
@ -10848,9 +10942,6 @@ bool DecodeMesh(nrhi::Device* device, uint8_t* base, const DrawItem& item,
|
||||||
if (!item.skinned && item.normal_fmt == 16) {
|
if (!item.skinned && item.normal_fmt == 16) {
|
||||||
float b3[3];
|
float b3[3];
|
||||||
unpack_10_11_11(item.normal_offset, b3);
|
unpack_10_11_11(item.normal_offset, b3);
|
||||||
const auto pk = [](float f) {
|
|
||||||
return uint32_t(std::lround((f * 0.5f + 0.5f) * 255.0f)) & 0xFFu;
|
|
||||||
};
|
|
||||||
// w byte is a three-state presence sentinel: 0 = no stored frame
|
// w byte is a three-state presence sentinel: 0 = no stored frame
|
||||||
// (meshes that skip this pack leave the word zero), 100 = negative
|
// (meshes that skip this pack leave the word zero), 100 = negative
|
||||||
// handedness, 200 = positive.
|
// handedness, 200 = positive.
|
||||||
|
|
@ -10866,6 +10957,24 @@ bool DecodeMesh(nrhi::Device* device, uint8_t* base, const DrawItem& item,
|
||||||
// interior verts where t is parallel to b and the cross vanishes,
|
// interior verts where t is parallel to b and the cross vanishes,
|
||||||
// measured in capture, so the stored normal is used.)
|
// measured in capture, so the stored normal is used.)
|
||||||
unpack_10_11_11(item.normal_offset, n3);
|
unpack_10_11_11(item.normal_offset, n3);
|
||||||
|
// Dynamicobject v2: rigid props store a full usage-6/7 tangent +
|
||||||
|
// binormal pair alongside the usage-3 normal. Pack the binormal plus
|
||||||
|
// the handedness relating cross(B, N) to the stored tangent into the
|
||||||
|
// unused blend-weight bytes (same sentinel encoding as the env
|
||||||
|
// families above); the authored frame carries the per-UV-island
|
||||||
|
// mirror decisions the screen-space fallback cannot know.
|
||||||
|
if (!item.skinned && item.dynobj != 0 && item.tb_fmt == 16) {
|
||||||
|
float t3[3], b3[3];
|
||||||
|
unpack_10_11_11(item.tangent_offset, t3);
|
||||||
|
unpack_10_11_11(item.binormal_offset, b3);
|
||||||
|
const float h = (b3[1] * n3[2] - b3[2] * n3[1]) * t3[0] +
|
||||||
|
(b3[2] * n3[0] - b3[0] * n3[2]) * t3[1] +
|
||||||
|
(b3[0] * n3[1] - b3[1] * n3[0]) * t3[2];
|
||||||
|
const uint32_t packed = pk(b3[0]) | (pk(b3[1]) << 8) |
|
||||||
|
(pk(b3[2]) << 16) |
|
||||||
|
((h >= 0.0f ? 200u : 100u) << 24);
|
||||||
|
std::memcpy(&dst[v * 14 + 7], &packed, 4);
|
||||||
|
}
|
||||||
} else if (item.tb_fmt == 16) {
|
} else if (item.tb_fmt == 16) {
|
||||||
// NPC character meshes carry no normal element; the game's VS
|
// NPC character meshes carry no normal element; the game's VS
|
||||||
// derives it as cross(tangent, binormal) (usage 6 x usage 7; verified
|
// derives it as cross(tangent, binormal) (usage 6 x usage 7; verified
|
||||||
|
|
@ -10878,11 +10987,31 @@ bool DecodeMesh(nrhi::Device* device, uint8_t* base, const DrawItem& item,
|
||||||
n3[0] = t3[1] * b3[2] - t3[2] * b3[1];
|
n3[0] = t3[1] * b3[2] - t3[2] * b3[1];
|
||||||
n3[1] = t3[2] * b3[0] - t3[0] * b3[2];
|
n3[1] = t3[2] * b3[0] - t3[0] * b3[2];
|
||||||
n3[2] = t3[0] * b3[1] - t3[1] * b3[0];
|
n3[2] = t3[0] * b3[1] - t3[1] * b3[0];
|
||||||
|
// Dynamicobject v2, tangent+binormal-only layout variant: the derived
|
||||||
|
// normal makes cross(B, N) == T x |B|^2 up to orthogonality error, so
|
||||||
|
// the handedness is positive by construction.
|
||||||
|
if (!item.skinned && item.dynobj != 0) {
|
||||||
|
const uint32_t packed = pk(b3[0]) | (pk(b3[1]) << 8) |
|
||||||
|
(pk(b3[2]) << 16) | (200u << 24);
|
||||||
|
std::memcpy(&dst[v * 14 + 7], &packed, 4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dst[v * 14 + 9] = n3[0];
|
dst[v * 14 + 9] = n3[0];
|
||||||
dst[v * 14 + 10] = n3[1];
|
dst[v * 14 + 10] = n3[1];
|
||||||
dst[v * 14 + 11] = n3[2];
|
dst[v * 14 + 11] = n3[2];
|
||||||
}
|
}
|
||||||
|
// Dynobj v2 layout telemetry (first few meshes): which tangent-frame
|
||||||
|
// source the props carry decides between the packed authored frame and
|
||||||
|
// the shader's screen-space fallback.
|
||||||
|
if (item.dynobj != 0) {
|
||||||
|
static std::atomic<int> dynobj_layout_logged{0};
|
||||||
|
if (dynobj_layout_logged.fetch_add(1, std::memory_order_relaxed) < 8) {
|
||||||
|
REXLOG_INFO(
|
||||||
|
"native-scene: dynobj mesh {:08X} layout normal_fmt={} tb_fmt={} "
|
||||||
|
"skinned={}",
|
||||||
|
item.mesh, item.normal_fmt, item.tb_fmt, item.skinned);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Stretch-veto probe (see g_skin_probe): cache ~32 decoded sample verts
|
// Stretch-veto probe (see g_skin_probe): cache ~32 decoded sample verts
|
||||||
// so the guest thread can cheaply skin what the GPU will actually draw.
|
// so the guest thread can cheaply skin what the GPU will actually draw.
|
||||||
if (item.skinned) {
|
if (item.skinned) {
|
||||||
|
|
@ -13330,10 +13459,37 @@ bool EnsureShadowResources(const NativeGuestOutputRenderContext& context) {
|
||||||
g_r.shadow_tile, g_r.shadow_tile,
|
g_r.shadow_tile, g_r.shadow_tile,
|
||||||
tile_cfg > 0 ? "" : ", auto");
|
tile_cfg > 0 ? "" : ", auto");
|
||||||
}
|
}
|
||||||
|
if (!g_r.world_shadow && g_r.shadow_raw != nullptr) {
|
||||||
|
// dynamicobject static world-shadow map (see RendererState). Fixed at
|
||||||
|
// the game's own 512x512: a coarse region-scale baked-shade map whose
|
||||||
|
// PCF footprint the props' shading was tuned against.
|
||||||
|
nrhi::TextureDesc desc;
|
||||||
|
desc.width = RendererState::kWorldShadowSize;
|
||||||
|
desc.height = RendererState::kWorldShadowSize;
|
||||||
|
desc.format = nrhi::Format::kR16G16_UNORM;
|
||||||
|
desc.usage = nrhi::kTextureUsageRenderTarget;
|
||||||
|
desc.initial_state = nrhi::ResourceState::kRenderTarget;
|
||||||
|
desc.clear_color[0] = 1.0f; // depth: far = lit
|
||||||
|
desc.clear_color[1] = 1.0f;
|
||||||
|
g_r.world_shadow = device->CreateTexture(desc);
|
||||||
|
if (g_r.world_shadow != nullptr) {
|
||||||
|
nrhi::TextureViewDesc vd;
|
||||||
|
vd.mip_levels = 1;
|
||||||
|
g_r.world_shadow_srv = device->CreateTextureView(g_r.world_shadow, vd);
|
||||||
|
}
|
||||||
|
if (g_r.world_shadow_srv == nullptr) {
|
||||||
|
REXLOG_ERROR("native-scene: world-shadow map creation failed");
|
||||||
|
g_r.failed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_r.world_shadow_in_srv = false;
|
||||||
|
g_r.world_shadow_primed = false;
|
||||||
|
}
|
||||||
if (!g_r.shadow_cb) {
|
if (!g_r.shadow_cb) {
|
||||||
// Always created (even with shadows off): the scene PS declares b1 and
|
// Always created (even with shadows off): the scene PS declares b1 and
|
||||||
// a root CBV must be bound; a zeroed block disables the shadow branch.
|
// a root CBV must be bound; a zeroed block disables the shadow branch.
|
||||||
g_r.shadow_cb = CreateUploadBuffer(device, 256u * RendererState::kShadowCbRegions);
|
g_r.shadow_cb = CreateUploadBuffer(
|
||||||
|
device, size_t(RendererState::kShadowCbSlice) * RendererState::kShadowCbRegions);
|
||||||
g_r.shadow_cb_cpu =
|
g_r.shadow_cb_cpu =
|
||||||
g_r.shadow_cb ? static_cast<uint8_t*>(device->Map(g_r.shadow_cb))
|
g_r.shadow_cb ? static_cast<uint8_t*>(device->Map(g_r.shadow_cb))
|
||||||
: nullptr;
|
: nullptr;
|
||||||
|
|
@ -14339,12 +14495,14 @@ void WarmItemResources(const NativeGuestOutputRenderContext& context, uint8_t* b
|
||||||
if (REXCVAR_GET(skate3_native_render_scene_macro)) {
|
if (REXCVAR_GET(skate3_native_render_scene_macro)) {
|
||||||
warm_texture(item.macro_tex);
|
warm_texture(item.macro_tex);
|
||||||
}
|
}
|
||||||
if (item.water || item.char_family >= 6 ||
|
if (item.water || item.char_family >= 6 || item.dynobj != 0 ||
|
||||||
(item.env_family >= 1 && item.env_family <= 6)) {
|
(item.env_family >= 1 && item.env_family <= 6)) {
|
||||||
// v2: fams 1-4 sample their base normal map per-pixel too.
|
// v2: fams 1-4 and the dynobj families sample their base normal map
|
||||||
|
// per-pixel too.
|
||||||
warm_texture(item.water_normal);
|
warm_texture(item.water_normal);
|
||||||
}
|
}
|
||||||
if (item.env_family >= 1 && item.env_family <= 4 && item.env_family != 2) {
|
if ((item.env_family >= 1 && item.env_family <= 4 && item.env_family != 2) ||
|
||||||
|
item.dynobj != 0) {
|
||||||
// v2 detail normal map (t8; fam 2 carries no base+detail pair).
|
// v2 detail normal map (t8; fam 2 carries no base+detail pair).
|
||||||
warm_texture(item.detail_tex);
|
warm_texture(item.detail_tex);
|
||||||
}
|
}
|
||||||
|
|
@ -14355,9 +14513,10 @@ void WarmItemResources(const NativeGuestOutputRenderContext& context, uint8_t* b
|
||||||
if (item.char_family >= 4 && item.char_family <= 5) {
|
if (item.char_family >= 4 && item.char_family <= 5) {
|
||||||
warm_texture(item.hair_alpha_tex);
|
warm_texture(item.hair_alpha_tex);
|
||||||
}
|
}
|
||||||
if ((item.env_family != 0 && item.env_family != 10) || item.unlit) {
|
if ((item.env_family != 0 && item.env_family != 10) || item.unlit ||
|
||||||
// unlit = sky: spec_tex is the 1D sun gradient. Decal fams (3/4) now
|
item.dynobj != 0) {
|
||||||
// consume their spec masks too (v2, t9).
|
// unlit = sky: spec_tex is the 1D sun gradient. Decal fams (3/4) and
|
||||||
|
// the dynobj families consume their spec masks too (v2, t9).
|
||||||
warm_texture(item.spec_tex);
|
warm_texture(item.spec_tex);
|
||||||
}
|
}
|
||||||
// Environment cube (negative-cached like the draw path).
|
// Environment cube (negative-cached like the draw path).
|
||||||
|
|
@ -14570,12 +14729,14 @@ void ProcessPrewarmEntry(uint8_t* base, const PrewarmEntry& e) {
|
||||||
if (REXCVAR_GET(skate3_native_render_scene_macro)) {
|
if (REXCVAR_GET(skate3_native_render_scene_macro)) {
|
||||||
stage_texture(item.macro_tex);
|
stage_texture(item.macro_tex);
|
||||||
}
|
}
|
||||||
if (item.water || item.char_family >= 6 ||
|
if (item.water || item.char_family >= 6 || item.dynobj != 0 ||
|
||||||
(item.env_family >= 1 && item.env_family <= 6)) {
|
(item.env_family >= 1 && item.env_family <= 6)) {
|
||||||
// v2: fams 1-4 sample their base normal map per-pixel too.
|
// v2: fams 1-4 and the dynobj families sample their base normal map
|
||||||
|
// per-pixel too.
|
||||||
stage_texture(item.water_normal);
|
stage_texture(item.water_normal);
|
||||||
}
|
}
|
||||||
if (item.env_family >= 1 && item.env_family <= 4 && item.env_family != 2) {
|
if ((item.env_family >= 1 && item.env_family <= 4 && item.env_family != 2) ||
|
||||||
|
item.dynobj != 0) {
|
||||||
// v2 detail normal map (t8; fam 2 carries no base+detail pair).
|
// v2 detail normal map (t8; fam 2 carries no base+detail pair).
|
||||||
stage_texture(item.detail_tex);
|
stage_texture(item.detail_tex);
|
||||||
}
|
}
|
||||||
|
|
@ -14585,9 +14746,10 @@ void ProcessPrewarmEntry(uint8_t* base, const PrewarmEntry& e) {
|
||||||
if (item.char_family >= 4 && item.char_family <= 5) {
|
if (item.char_family >= 4 && item.char_family <= 5) {
|
||||||
stage_texture(item.hair_alpha_tex);
|
stage_texture(item.hair_alpha_tex);
|
||||||
}
|
}
|
||||||
if ((item.env_family != 0 && item.env_family != 10) || item.unlit) {
|
if ((item.env_family != 0 && item.env_family != 10) || item.unlit ||
|
||||||
// unlit = sky: spec_tex is the 1D sun gradient. Decal fams (3/4) now
|
item.dynobj != 0) {
|
||||||
// consume their spec masks too (v2, t9).
|
// unlit = sky: spec_tex is the 1D sun gradient. Decal fams (3/4) and
|
||||||
|
// the dynobj families consume their spec masks too (v2, t9).
|
||||||
stage_texture(item.spec_tex);
|
stage_texture(item.spec_tex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16253,6 +16415,122 @@ bool RenderShadowAtlas(const NativeGuestOutputRenderContext& context,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ---- dynamicobject static world-shadow map ----
|
||||||
|
// The game's props sample a 512x512 baked-shade depth map (tf1) the guest
|
||||||
|
// never renders in native mode (its memory stays zeroed). Re-render it
|
||||||
|
// natively from the frame's STATIC world items with the captured c5/c6/c7
|
||||||
|
// projection, ps_shadow_caster convention (x = light-space depth, MIN
|
||||||
|
// blend). The map ACCUMULATES across frames without clearing: the item
|
||||||
|
// list is view-culled, and dropping off-screen casters would flicker
|
||||||
|
// props' shade with the camera; static geometry seen once stays in the
|
||||||
|
// map until the transform changes (streaming region switch), which
|
||||||
|
// re-primes it.
|
||||||
|
if (g_r.world_shadow != nullptr && g_r.pso_shadow_caster != nullptr &&
|
||||||
|
debug_mode == 0 && scene.dynobj_ws_valid &&
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_shadows) &&
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_dynobj_v2)) {
|
||||||
|
bool changed = !g_r.world_shadow_primed;
|
||||||
|
for (int k = 0; k < 12 && !changed; ++k) {
|
||||||
|
changed = std::fabs(scene.dynobj_ws[k] - g_r.world_shadow_rows[k]) >
|
||||||
|
1e-4f * std::max(1.0f, std::fabs(g_r.world_shadow_rows[k]));
|
||||||
|
}
|
||||||
|
static uint64_t ws_pass_counter = 0;
|
||||||
|
++ws_pass_counter;
|
||||||
|
// Accumulation cadence: a full static-item pass every 4th frame keeps
|
||||||
|
// newly streamed / newly visible geometry flowing into the map at
|
||||||
|
// negligible steady-state cost; a transform change renders immediately.
|
||||||
|
if (changed || (ws_pass_counter & 3u) == 0) {
|
||||||
|
if (g_r.world_shadow_in_srv) {
|
||||||
|
cmd->Barrier(g_r.world_shadow, nrhi::ResourceState::kPixelShaderResource,
|
||||||
|
nrhi::ResourceState::kRenderTarget);
|
||||||
|
cmd->FlushBarriers();
|
||||||
|
g_r.world_shadow_in_srv = false;
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
const float ws_clear[4] = {1.0f, 1.0f, 0.0f, 0.0f};
|
||||||
|
cmd->ClearRenderTarget(g_r.world_shadow, ws_clear);
|
||||||
|
std::memcpy(g_r.world_shadow_rows, scene.dynobj_ws,
|
||||||
|
sizeof(g_r.world_shadow_rows));
|
||||||
|
if (!g_r.world_shadow_primed) {
|
||||||
|
REXLOG_INFO(
|
||||||
|
"native-scene: world-shadow map primed (dynobj c5/c6/c7 rows)");
|
||||||
|
}
|
||||||
|
g_r.world_shadow_primed = true;
|
||||||
|
}
|
||||||
|
// View-proj columns = the game's own projection rows: clip.x/y/z =
|
||||||
|
// dot(wp4, c5/c6/c7), clip.w = 1 (same construction as the CSM
|
||||||
|
// lightvp above).
|
||||||
|
float wsvp[16];
|
||||||
|
for (int r = 0; r < 3; ++r) {
|
||||||
|
wsvp[r * 4 + 0] = scene.dynobj_ws[r];
|
||||||
|
wsvp[r * 4 + 1] = scene.dynobj_ws[4 + r];
|
||||||
|
wsvp[r * 4 + 2] = scene.dynobj_ws[8 + r];
|
||||||
|
wsvp[r * 4 + 3] = 0.0f;
|
||||||
|
}
|
||||||
|
wsvp[12] = scene.dynobj_ws[3];
|
||||||
|
wsvp[13] = scene.dynobj_ws[7];
|
||||||
|
wsvp[14] = scene.dynobj_ws[11];
|
||||||
|
wsvp[15] = 1.0f;
|
||||||
|
cmd->SetRenderTargets(g_r.world_shadow, nullptr);
|
||||||
|
cmd->SetBindingLayout(g_r.layout);
|
||||||
|
cmd->SetPipeline(g_r.pso_shadow_caster);
|
||||||
|
cmd->SetConstantBuffer(9, g_r.bone_ring, bone_region);
|
||||||
|
const float ws_size = float(RendererState::kWorldShadowSize);
|
||||||
|
cmd->SetViewport(nrhi::Viewport{0.0f, 0.0f, ws_size, ws_size, 0.0f, 1.0f});
|
||||||
|
cmd->SetScissor(nrhi::Rect{0, 0, int32_t(RendererState::kWorldShadowSize),
|
||||||
|
int32_t(RendererState::kWorldShadowSize)});
|
||||||
|
for (const DrawItem& item : scene.items) {
|
||||||
|
// STATIC world geometry only: the game's map bakes buildings/trees/
|
||||||
|
// ground. Dynamic content (characters, vehicles, cloth, movable
|
||||||
|
// props) casts through the CSM instead, accumulating a movable
|
||||||
|
// prop here would freeze its shade at a stale position.
|
||||||
|
if (item.transparent || item.unlit || item.cloth_quads || item.water ||
|
||||||
|
item.skinned || item.ropa || item.char_family != 0 ||
|
||||||
|
item.dynobj != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto mit = g_r.meshes.find(item.mesh);
|
||||||
|
if (mit == g_r.meshes.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float constants[52] = {};
|
||||||
|
std::memcpy(constants, item.world, sizeof(item.world));
|
||||||
|
float* mvp = constants + 16;
|
||||||
|
for (int r = 0; r < 4; ++r) {
|
||||||
|
for (int col = 0; col < 4; ++col) {
|
||||||
|
float sum = 0.0f;
|
||||||
|
for (int k = 0; k < 4; ++k) {
|
||||||
|
sum += item.world[r * 4 + k] * wsvp[k * 4 + col];
|
||||||
|
}
|
||||||
|
mvp[r * 4 + col] = sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd->SetRootConstants(0, 52, constants, 0);
|
||||||
|
cmd->SetBufferSrv(3, g_r.bone_ring, bone_region);
|
||||||
|
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 : 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd->Barrier(g_r.world_shadow, nrhi::ResourceState::kRenderTarget,
|
||||||
|
nrhi::ResourceState::kPixelShaderResource);
|
||||||
|
cmd->FlushBarriers();
|
||||||
|
g_r.world_shadow_in_srv = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
*out_draws = shadow_draws;
|
*out_draws = shadow_draws;
|
||||||
return shadow_ready;
|
return shadow_ready;
|
||||||
}
|
}
|
||||||
|
|
@ -16848,9 +17126,10 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
// shader is also gated by sh_misc.y).
|
// shader is also gated by sh_misc.y).
|
||||||
if (g_r.shadow_cb != nullptr) {
|
if (g_r.shadow_cb != nullptr) {
|
||||||
const uint32_t cb_offset =
|
const uint32_t cb_offset =
|
||||||
uint32_t(frame_number % RendererState::kShadowCbRegions) * 256u;
|
uint32_t(frame_number % RendererState::kShadowCbRegions) *
|
||||||
|
RendererState::kShadowCbSlice;
|
||||||
float* cb = reinterpret_cast<float*>(g_r.shadow_cb_cpu + cb_offset);
|
float* cb = reinterpret_cast<float*>(g_r.shadow_cb_cpu + cb_offset);
|
||||||
std::memset(cb, 0, 256);
|
std::memset(cb, 0, RendererState::kShadowCbSlice);
|
||||||
if (shadow_ready) {
|
if (shadow_ready) {
|
||||||
std::memcpy(cb + 0, sh + 0, 4 * sizeof(float)); // sh_x = PS c0
|
std::memcpy(cb + 0, sh + 0, 4 * sizeof(float)); // sh_x = PS c0
|
||||||
std::memcpy(cb + 4, sh + 12, 4 * sizeof(float)); // sh_y = PS c3
|
std::memcpy(cb + 4, sh + 12, 4 * sizeof(float)); // sh_y = PS c3
|
||||||
|
|
@ -16912,6 +17191,12 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
cb[51] = scene.dynobj_rows[7]; // bounce scale (c15.w)
|
cb[51] = scene.dynobj_rows[7]; // bounce scale (c15.w)
|
||||||
cb[52] = scene.dynobj_rows[8]; // material multiplier (c14.y)
|
cb[52] = scene.dynobj_rows[8]; // material multiplier (c14.y)
|
||||||
cb[53] = scene.dynobj_rows[9]; // static world-shadow floor (c8.w)
|
cb[53] = scene.dynobj_rows[9]; // static world-shadow floor (c8.w)
|
||||||
|
// Static world-shadow transform rows (dyn_wsx/dyn_wsy/dyn_wsz,
|
||||||
|
// cb[64..75]): consumed only when a draw carries the misc.z world-
|
||||||
|
// shadow bind flag (the map primed and bound at t4).
|
||||||
|
if (scene.dynobj_ws_valid) {
|
||||||
|
std::memcpy(cb + 64, scene.dynobj_ws, sizeof(scene.dynobj_ws));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cmd->SetConstantBuffer(6, g_r.shadow_cb, cb_offset);
|
cmd->SetConstantBuffer(6, g_r.shadow_cb, cb_offset);
|
||||||
// b2 (character lighting) default: point at the ring base so the root
|
// b2 (character lighting) default: point at the ring base so the root
|
||||||
|
|
@ -17899,6 +18184,49 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Dynamicobject v2: the same misc.z/w convention and t5/t8/t9 slots as
|
||||||
|
// fams 1-4; dynobj draws are never transparent/water and carry
|
||||||
|
// env_family 0, so both misc slots are free, and t4 stays on its white
|
||||||
|
// fallback (the spec-mask t4 bind is env-family-only), so the t5 pair
|
||||||
|
// costs nothing. The spec/ecc masks go to t9 rather than t4: it keeps
|
||||||
|
// t4's semantics untouched and matches the decal families' 2-channel
|
||||||
|
// mask convention. Gated exactly like the PS branch selection (dynobj
|
||||||
|
// frame rows captured) plus the dynobj_v2 cvar for A/B against the v1
|
||||||
|
// flat response.
|
||||||
|
if (item.dynobj != 0 && debug_mode == 0 && scene.dynobj_valid &&
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_dynobj_v2)) {
|
||||||
|
if (item.water_normal != 0) {
|
||||||
|
const GuestTexture* nrm = resolve_texture(item.water_normal, 3);
|
||||||
|
if (nrm != &g_r.white && nrm->valid && nrm->srv_mips != 0) {
|
||||||
|
pair_normal = nrm;
|
||||||
|
normal_paired = true;
|
||||||
|
v2_flags |= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((v2_flags & 1u) != 0 && item.detail_tex != 0 &&
|
||||||
|
item.detail_scale > 0.0f) {
|
||||||
|
const GuestTexture* det = resolve_texture(item.detail_tex, 7);
|
||||||
|
if (det != &g_r.white && det->valid && det->srv_mips != 0) {
|
||||||
|
v2_detail = det;
|
||||||
|
v2_flags |= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.spec_tex != 0) {
|
||||||
|
const GuestTexture* sp = resolve_texture(item.spec_tex, 6);
|
||||||
|
if (sp != &g_r.white && sp->valid && sp->srv_mips != 0) {
|
||||||
|
v2_spec2 = sp;
|
||||||
|
v2_flags |= 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Static world-shadow map at t4 (flag 8): the native re-render of the
|
||||||
|
// game's baked-shade map (see RenderShadowAtlas). t4 is otherwise the
|
||||||
|
// white fallback on dynobj draws. Without it the PS world term is 1
|
||||||
|
// and props inside baked building shade light at full key.
|
||||||
|
if (g_r.world_shadow_srv != nullptr && g_r.world_shadow_in_srv &&
|
||||||
|
scene.dynobj_ws_valid) {
|
||||||
|
v2_flags |= 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
constants[44] = item.macro_scale;
|
constants[44] = item.macro_scale;
|
||||||
constants[45] = item.macro_opacity;
|
constants[45] = item.macro_opacity;
|
||||||
constants[46] = macro_tex != &g_r.white ? 1.0f : 0.0f;
|
constants[46] = macro_tex != &g_r.white ? 1.0f : 0.0f;
|
||||||
|
|
@ -17945,7 +18273,7 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
std::memcpy(constants + 40, scene.fog_color, 4 * sizeof(float));
|
std::memcpy(constants + 40, scene.fog_color, 4 * sizeof(float));
|
||||||
} else if (v2_flags != 0) {
|
} else if (v2_flags != 0) {
|
||||||
// misc.z = the v2 material bind flags, misc.w = detailNormalUVScale
|
// misc.z = the v2 material bind flags, misc.w = detailNormalUVScale
|
||||||
// (fams 1-4 opaque; see the v2 block above).
|
// (opaque fams 1-4 and the dynobj families; see the v2 blocks above).
|
||||||
constants[50] = float(v2_flags);
|
constants[50] = float(v2_flags);
|
||||||
constants[51] = item.detail_scale;
|
constants[51] = item.detail_scale;
|
||||||
} else if ((item.env_family >= 5 && item.env_family <= 6) ||
|
} else if ((item.env_family >= 5 && item.env_family <= 6) ||
|
||||||
|
|
@ -18048,8 +18376,14 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
cmd->SetTexture(1, diffuse->srv);
|
cmd->SetTexture(1, diffuse->srv);
|
||||||
cmd->SetTexture(2, (lightmap != nullptr ? lightmap : &g_r.white)->srv);
|
cmd->SetTexture(2, (lightmap != nullptr ? lightmap : &g_r.white)->srv);
|
||||||
cmd->SetTexture(4, macro_tex->srv);
|
cmd->SetTexture(4, macro_tex->srv);
|
||||||
if (normal_paired) {
|
// t4 override: the dynobj world-shadow map rides the free first entry
|
||||||
cmd->SetTexturePair(5, decal_tex->srv, pair_normal->srv);
|
// of the t4/t5 pair table (flag 8: dynobj draws never bind decal art
|
||||||
|
// or spec masks there).
|
||||||
|
nrhi::TextureView* t4_view =
|
||||||
|
(v2_flags & 8u) != 0 ? g_r.world_shadow_srv : decal_tex->srv;
|
||||||
|
if (normal_paired || t4_view != decal_tex->srv) {
|
||||||
|
cmd->SetTexturePair(5, t4_view,
|
||||||
|
normal_paired ? pair_normal->srv : g_r.white.srv);
|
||||||
} else {
|
} else {
|
||||||
cmd->SetTexture(5, decal_tex->srv);
|
cmd->SetTexture(5, decal_tex->srv);
|
||||||
}
|
}
|
||||||
|
|
@ -18736,7 +19070,8 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
cmd->SetBindingLayout(g_r.layout);
|
cmd->SetBindingLayout(g_r.layout);
|
||||||
if (g_r.shadow_cb != nullptr) {
|
if (g_r.shadow_cb != nullptr) {
|
||||||
const uint32_t cb_offset =
|
const uint32_t cb_offset =
|
||||||
uint32_t(frame_number % RendererState::kShadowCbRegions) * 256u;
|
uint32_t(frame_number % RendererState::kShadowCbRegions) *
|
||||||
|
RendererState::kShadowCbSlice;
|
||||||
cmd->SetConstantBuffer(6, g_r.shadow_cb, cb_offset);
|
cmd->SetConstantBuffer(6, g_r.shadow_cb, cb_offset);
|
||||||
cmd->SetConstantBuffer(9, g_r.bone_ring, bone_region);
|
cmd->SetConstantBuffer(9, g_r.bone_ring, bone_region);
|
||||||
}
|
}
|
||||||
|
|
@ -19129,7 +19464,8 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
cmd->SetBindingLayout(g_r.layout);
|
cmd->SetBindingLayout(g_r.layout);
|
||||||
if (g_r.shadow_cb != nullptr) {
|
if (g_r.shadow_cb != nullptr) {
|
||||||
const uint32_t cb_offset =
|
const uint32_t cb_offset =
|
||||||
uint32_t(frame_number % RendererState::kShadowCbRegions) * 256u;
|
uint32_t(frame_number % RendererState::kShadowCbRegions) *
|
||||||
|
RendererState::kShadowCbSlice;
|
||||||
cmd->SetConstantBuffer(6, g_r.shadow_cb, cb_offset);
|
cmd->SetConstantBuffer(6, g_r.shadow_cb, cb_offset);
|
||||||
cmd->SetConstantBuffer(9, g_r.bone_ring, bone_region);
|
cmd->SetConstantBuffer(9, g_r.bone_ring, bone_region);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -303,6 +303,17 @@ struct FrameScene {
|
||||||
// Consumed by the exact dynamicobject scene PS branch.
|
// Consumed by the exact dynamicobject scene PS branch.
|
||||||
bool dynobj_valid = false;
|
bool dynobj_valid = false;
|
||||||
float dynobj_rows[10] = {};
|
float dynobj_rows[10] = {};
|
||||||
|
// dynamicobject.fx static world-shadow transform (PS c5/c6/c7, captured
|
||||||
|
// with the rows above): world-space projection rows into the game's
|
||||||
|
// 512x512 baked-shade map (uv = dot(wp4, c5/c6) * (+-0.5) + 0.5, ray
|
||||||
|
// depth = dot(wp4, c7)). The guest never renders that map in native mode
|
||||||
|
// (its memory stays zeroed), so the native path re-renders it from the
|
||||||
|
// frame's STATIC world items with these rows (see RenderShadowAtlas) and
|
||||||
|
// props sample it exactly like the game's PS: shadow = min(csm * gate,
|
||||||
|
// max(world, c8.w)); without it props inside baked building shade lit
|
||||||
|
// at full key (the newspaper-machine mint brightening).
|
||||||
|
bool dynobj_ws_valid = false;
|
||||||
|
float dynobj_ws[12] = {};
|
||||||
// Character-receiver per-cascade CSM receive biases, captured from a
|
// Character-receiver per-cascade CSM receive biases, captured from a
|
||||||
// validated fam-2 character PIXEL bank (gameplay c8 / editor c9: one row
|
// validated fam-2 character PIXEL bank (gameplay c8 / editor c9: one row
|
||||||
// below the light row; frame-global, e.g. (0.005, 0.014, 0.020)). The
|
// below the light row; frame-global, e.g. (0.005, 0.014, 0.020)). The
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue