render: character normal maps and specular lighting
This commit is contained in:
parent
01e2b51810
commit
275fa431c3
7 changed files with 15189 additions and 14818 deletions
|
|
@ -49,7 +49,11 @@ cbuffer CH : register(b2) {
|
|||
// vehicles keep spec color + power in row 0 instead
|
||||
float4 ch_tintA; // CAC diffuse tint / livingworld red-mask tint (w = apply)
|
||||
float4 ch_tintB; // livingworld blue-mask tint / hair fresnel tint (w = strand-alpha scale)
|
||||
float4 ch_misc; // x = alpha out, y = family
|
||||
float4 ch_misc; // x = alpha out, y = family, z = lens-alpha flag,
|
||||
// w = rim fresnel power
|
||||
float4 ch_ks; // rgb = key spec color, w = power (0 = spec rows absent)
|
||||
float4 ch_rs; // rgb = rim spec color, w = power
|
||||
float4 ch_rim; // rgb = rim light color, w = key-spec fresnel power
|
||||
};
|
||||
Texture2D<float4> diffuse : register(t0);
|
||||
Texture2D<float4> lightmap : register(t1);
|
||||
|
|
|
|||
|
|
@ -107,18 +107,80 @@ float4 ShadeCharacter(VSOut i, float4 albedo) {
|
|||
} else {
|
||||
// defaultcharacter / CAC pieces: key light + SH irradiance ambient,
|
||||
// key gated by the CSM shadow (see the livingworld comment above).
|
||||
float csm = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, 0.012, cn, i.pos.xy),
|
||||
SampleStaticSun(i.rpos + cam_pos.xyz, cn, i.pos.xy));
|
||||
// The material's DXT5nm normal map (x in ALPHA, y in GREEN; the PSes
|
||||
// read tf4.wy) rides the macro slot; overlay.z > 0 = resolved. The
|
||||
// skinned vertex layout has no free bytes for the authored tangent
|
||||
// frame (blend weights/indices own them), so use the screen-space
|
||||
// cotangent frame. ScreenTangentFrame's env calibration (negated U
|
||||
// axis, raw V axis) holds for characters too: projecting the skinned
|
||||
// meshes through the frame's real view_proj and taking the D3D y-down
|
||||
// screen-basis derivatives on the CAMERA-FACING triangles dots the
|
||||
// frame against the skinned usage-6/7 tangent frame at -0.95 (tt.T) /
|
||||
// +0.95 (bb.B); an edge-basis check without the projection lands on
|
||||
// the back-face orientation and reads inverted. Map x rides T, map y
|
||||
// rides B (the VS skins the usage-6 tangent into the interpolator the
|
||||
// PS pairs with tf4.w, the usage-7 binormal into the tf4.y one).
|
||||
float3 vn = cn;
|
||||
if (overlay.z > 0.5) {
|
||||
// misc.y = LOD bias to the console's 640p-gradient mip (same
|
||||
// rationale as the reflective families' cube bias): mip 0 at 4K
|
||||
// keeps fine wrinkle noise the console filters away, which reads
|
||||
// as weaker authored folds than the emulated reference.
|
||||
float2 nm = macro.SampleBias(smp, i.uv, misc.y).ag * 2.0 - 1.0;
|
||||
float3 tt, bb;
|
||||
ScreenTangentFrame(cn, i.rpos, i.uv, tt, bb);
|
||||
vn = normalize(nm.x * tt + nm.y * bb +
|
||||
cn * sqrt(saturate(1.0 - dot(nm, nm))));
|
||||
}
|
||||
float vndl_u = dot(vn, ch_light.xyz);
|
||||
float vndl = saturate(vndl_u);
|
||||
float csm = min(SampleCsmShadowSoft(i.rpos + cam_pos.xyz, 0.012, vn, i.pos.xy),
|
||||
SampleStaticSun(i.rpos + cam_pos.xyz, vn, i.pos.xy));
|
||||
if (ch_tintA.w > 0.0) {
|
||||
dlin *= ch_tintA.rgb;
|
||||
}
|
||||
float3 irr = saturate(
|
||||
ch_sh[0].rgb + cn.x * ch_sh[1].rgb + cn.y * ch_sh[2].rgb +
|
||||
cn.z * ch_sh[3].rgb + (cn.x * cn.z) * ch_sh[4].rgb +
|
||||
(cn.z * cn.y) * ch_sh[5].rgb + (cn.y * cn.x) * ch_sh[6].rgb +
|
||||
(cn.z * cn.z) * ch_sh[7].rgb +
|
||||
(cn.x * cn.x - cn.y * cn.y) * ch_sh[8].rgb);
|
||||
lin = dlin * (ch_key.rgb * ndl * csm + irr * ch_amb.w);
|
||||
ch_sh[0].rgb + vn.x * ch_sh[1].rgb + vn.y * ch_sh[2].rgb +
|
||||
vn.z * ch_sh[3].rgb + (vn.x * vn.z) * ch_sh[4].rgb +
|
||||
(vn.z * vn.y) * ch_sh[5].rgb + (vn.y * vn.x) * ch_sh[6].rgb +
|
||||
(vn.z * vn.z) * ch_sh[7].rgb +
|
||||
(vn.x * vn.x - vn.y * vn.y) * ch_sh[8].rgb);
|
||||
float3 lit = ch_key.rgb * vndl * csm + irr * ch_amb.w;
|
||||
float3 spec = float3(0.0, 0.0, 0.0);
|
||||
// Rim light + key/rim phong specular, exact from the character PSes
|
||||
// (ch_ks.w == 0 = rows not captured -> the terms vanish). The key spec
|
||||
// reflects the sun about the mapped normal, gated on N.L >= 0 and the
|
||||
// shadow; the rim terms share the game's fixed rim direction built
|
||||
// from the sun and view. Spec mask = the diffuse alpha SQUARED
|
||||
// (linear-space decode, like the vehicle gloss); skin/face carry a
|
||||
// dedicated mask map in the free decal slot instead (overlay.w = 3;
|
||||
// 2 = map present but not yet decoded -> mask 0, because the DXT1
|
||||
// skin diffuse's opaque alpha would read as a full-white mask).
|
||||
if (ch_ks.w > 0.0) {
|
||||
float fb = 1.0 - saturate(dot(vn, vd));
|
||||
float kfres = pow(max(fb, 1e-6), ch_rim.w);
|
||||
float rfres = pow(max(fb, 1e-6), ch_misc.w);
|
||||
float3 rd = normalize(ch_light.xyz * float3(-1.0, 0.2, -1.0) - vd);
|
||||
lit += saturate(rfres * saturate(dot(vn, rd))) * ch_rim.rgb;
|
||||
float3 kr = ch_light.xyz - 2.0 * vndl_u * vn;
|
||||
float ks = pow(saturate(dot(vd, -kr)), ch_ks.w) * csm *
|
||||
(vndl_u >= 0.0 ? 1.0 : 0.0);
|
||||
float3 rr = rd - 2.0 * dot(vn, rd) * vn;
|
||||
float rs = pow(saturate(dot(vd, -rr)), ch_rs.w) * rfres;
|
||||
float smask = overlay.w > 2.5 ? decal_art.SampleBias(smp, i.uv, misc.y).r
|
||||
: (overlay.w > 1.5 ? 0.0 : albedo.a);
|
||||
smask *= smask;
|
||||
spec = saturate((ks * ch_ks.rgb * kfres + rs * ch_rs.rgb) * smask);
|
||||
}
|
||||
lin = dlin * lit + spec;
|
||||
// The PS multiplies the lit color by the material multiplier
|
||||
// m_params[0].y before the tone chain (1.2 on the gameplay banks;
|
||||
// without it the jeans sit ~14% darker than the emulated reference).
|
||||
// Captured into the otherwise-unused tintB.w on fams 1/2; 0 = an
|
||||
// older capture without it.
|
||||
if (ch_tintB.w > 0.25) {
|
||||
lin *= ch_tintB.w;
|
||||
}
|
||||
// defaultcharacter/cacstamp PSes end `max oC0.w, c13.x / c22.x`:
|
||||
// the entity's spawn fade (see the livingworld comment above).
|
||||
out_a = ch_misc.x;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3242,11 +3242,18 @@ void CaptureHairTint(uint8_t* base, DrawItem& item) {
|
|||
// Ny^2)*r8); the scale and the -1 are folded into the stored rows so the
|
||||
// shader evaluates a plain 9-row basis.
|
||||
//
|
||||
// Canonical block (15 float4 rows): [0] = light dir + hair fresnel power,
|
||||
// Canonical block (18 float4 rows): [0] = light dir + hair fresnel power,
|
||||
// [1] = key color + exposure, [2] = flat ambient rgb + SH-ambient
|
||||
// multiplier (hair ambient scalar in w), [3..11] = SH rows, [12] = tintA
|
||||
// (w = apply), [13] = tintB + strand-alpha scale, [14].x = alpha out,
|
||||
// [14].y = family (0 = capture failed validation -> legacy shading).
|
||||
// (w = apply), [13] = tintB + strand-alpha scale (fams 1/2: w = the
|
||||
// material multiplier m_params[0].y instead), [14].x = alpha out,
|
||||
// [14].y = family (0 = capture failed validation -> legacy shading),
|
||||
// [14].z = lens-alpha flag, [14].w = rim fresnel power, [15] = key spec
|
||||
// color + power, [16] = rim spec color + power, [17] = rim light color +
|
||||
// key-spec fresnel power. The spec/rim rows (defaultcharacter: spec c7,
|
||||
// rim spec c8, rim c11, fresnel powers c6.w/c5.z; cacstamp: c16/c17/c20,
|
||||
// c15.w/c14.z [+ the editor row shift]) exist on fams 1/2 only; [15].w
|
||||
// stays 0 when they fail their range gates and the PS terms vanish.
|
||||
void CaptureCharLighting(uint8_t* base, DrawItem& item) {
|
||||
if (item.char_family == 0) {
|
||||
return;
|
||||
|
|
@ -3263,7 +3270,7 @@ void CaptureCharLighting(uint8_t* base, DrawItem& item) {
|
|||
// later draw with the same buffers (the caster-pass bank is stale; its
|
||||
// shadowPS touches no PS constants), and a failed refresh must not wipe
|
||||
// rows a previous successful capture staged.
|
||||
float local[60];
|
||||
float local[72];
|
||||
float* d = local;
|
||||
std::memset(local, 0, sizeof(local));
|
||||
uint8_t fam = item.char_family;
|
||||
|
|
@ -3379,6 +3386,51 @@ void CaptureCharLighting(uint8_t* base, DrawItem& item) {
|
|||
d[12 * 4 + 3] = 1.0f;
|
||||
}
|
||||
}
|
||||
// Material multiplier m_params[0].y (defaultcharacter c5.y / cacstamp
|
||||
// c14.y): the PS multiplies the lit color by it before the tone chain
|
||||
// (1.2 on the gameplay banks). Rides the fam-1/2-unused tintB.w row;
|
||||
// out-of-range keeps 0 and the PS applies 1.
|
||||
{
|
||||
const float m0y = row((fam == 1 ? 5u : 14u) + plus, 1);
|
||||
if (m0y > 0.25f && m0y < 4.0f) {
|
||||
d[13 * 4 + 3] = m0y;
|
||||
}
|
||||
}
|
||||
// Rim light + key/rim phong spec rows (see the header comment; observed
|
||||
// gameplay banks: key spec white pow 5, rim spec gray pow 67/255, rim
|
||||
// 0.2, fresnel powers 0.2-2.0). All-or-nothing behind range gates;
|
||||
// a bank that fails them keeps [15].w at 0 and the PS skips the terms.
|
||||
{
|
||||
const uint32_t ks_r = (fam == 1 ? 7u : 16u) + plus;
|
||||
const uint32_t rs_r = (fam == 1 ? 8u : 17u) + plus;
|
||||
const uint32_t rim_r = (fam == 1 ? 11u : 20u) + plus;
|
||||
const float kfres = row((fam == 1 ? 6u : 15u) + plus, 3);
|
||||
const float rfres = row((fam == 1 ? 5u : 14u) + plus, 2);
|
||||
float sr[12];
|
||||
bool spec_ok = kfres >= 0.0f && kfres < 512.0f &&
|
||||
rfres >= 0.0f && rfres < 512.0f;
|
||||
for (int r = 0; r < 3 && spec_ok; ++r) {
|
||||
const uint32_t src = r == 0 ? ks_r : (r == 1 ? rs_r : rim_r);
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
sr[r * 4 + c] = row(src, uint32_t(c));
|
||||
spec_ok = spec_ok && sr[r * 4 + c] >= 0.0f && sr[r * 4 + c] < 64.0f;
|
||||
}
|
||||
sr[r * 4 + 3] = row(src, 3);
|
||||
}
|
||||
spec_ok = spec_ok && sr[3] >= 0.5f && sr[3] < 512.0f &&
|
||||
sr[7] >= 0.5f && sr[7] < 512.0f;
|
||||
if (spec_ok) {
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
d[15 * 4 + c] = sr[c];
|
||||
d[16 * 4 + c] = sr[4 + c];
|
||||
}
|
||||
d[17 * 4 + 0] = sr[8];
|
||||
d[17 * 4 + 1] = sr[9];
|
||||
d[17 * 4 + 2] = sr[10];
|
||||
d[17 * 4 + 3] = kfres;
|
||||
d[14 * 4 + 3] = rfres;
|
||||
}
|
||||
}
|
||||
d[14 * 4 + 0] = std::clamp(row((fam == 1 ? 13u : 22u) + plus, 0), 0.0f, 1.0f);
|
||||
// character.alpha accessory (sunglass lens): tell the PS fam-1/2 branch
|
||||
// to take alpha from the coverage texture at t4/uv2 (ch_misc.z); the
|
||||
|
|
|
|||
|
|
@ -109,11 +109,11 @@ struct DrawItem {
|
|||
// windows). 0 = not a character.
|
||||
// char_rows = the canonical per-draw lighting block captured from the
|
||||
// PIXEL constant bank at palette-capture time (CaptureCharLighting):
|
||||
// 15 float4 rows consumed by the scene PS character branch (cbuffer CH,
|
||||
// 18 float4 rows consumed by the scene PS character branch (cbuffer CH,
|
||||
// b2). rows[14].y = family, > 0 only when the capture validated; items
|
||||
// without it fall back to the legacy empirical character shading.
|
||||
uint8_t char_family = 0;
|
||||
float char_rows[60] = {};
|
||||
float char_rows[72] = {};
|
||||
// Hair strand coverage: the hair mesh's "alpha" channel texture, sampled
|
||||
// at the SECOND texcoord (raw float2); hair renders alpha-blended in the
|
||||
// sorted sub-pass (the opaque path is the blocky-helmet look).
|
||||
|
|
|
|||
|
|
@ -7986,10 +7986,12 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
if (debug_mode == 0 && item.char_family != 0 &&
|
||||
item.char_rows[14 * 4 + 1] > 0.0f) {
|
||||
const uint32_t offset = (g_r.bone_ring_offset + 255u) & ~255u;
|
||||
if (offset + 256u <= RendererState::kBoneRegionSize) {
|
||||
// 18 float4 rows = 288 bytes -> a 512-byte slot keeps the next
|
||||
// allocation 256-aligned (CBV offset requirement).
|
||||
if (offset + 512u <= RendererState::kBoneRegionSize) {
|
||||
std::memcpy(g_r.bone_ring_cpu + bone_region + offset, item.char_rows,
|
||||
sizeof(item.char_rows));
|
||||
g_r.bone_ring_offset = offset + 256u;
|
||||
g_r.bone_ring_offset = offset + 512u;
|
||||
cmd->SetConstantBuffer(9, g_r.bone_ring, bone_region + offset);
|
||||
constants[39] = item.char_rows[14 * 4 + 1];
|
||||
g_char_drawn.fetch_add(1, std::memory_order_relaxed);
|
||||
|
|
@ -8025,7 +8027,9 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
item.macro_tex != 0 && REXCVAR_GET(skate3_native_render_scene_macro)
|
||||
? resolve_texture(item.macro_tex, 2)
|
||||
: &g_r.white;
|
||||
if ((item.water || item.char_family >= 6) && item.water_normal != 0) {
|
||||
if ((item.water || item.char_family >= 6 ||
|
||||
(item.char_family >= 1 && item.char_family <= 2)) &&
|
||||
item.water_normal != 0) {
|
||||
// Water rides its ripple normal map in the macro slot (water never
|
||||
// carries a macro overlay; overlay.z stays 0 below so the macro
|
||||
// composite path never runs). Vehicles do the same with their DXN
|
||||
|
|
@ -8033,6 +8037,10 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
// face away from the sun and shade as a dark ambient-blue patch that
|
||||
// stops at the door seams (the exact PS with a FLAT map reproduces
|
||||
// that artifact; with the real map it matches the emulated car).
|
||||
// Character fams 1/2 (defaultcharacter / CAC skin, face, cloth,
|
||||
// lenses) do the same with their DXT5nm `normal` channel, the
|
||||
// garment crease / skin pore detail the emulated render shows;
|
||||
// overlay.z > 0 tells the char branch the map resolved.
|
||||
macro_tex = resolve_texture(item.water_normal, 3);
|
||||
}
|
||||
// Water / vehicle environment cube (t6, root param 8): decoded once per
|
||||
|
|
@ -8154,6 +8162,24 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
sky_sun_bound = true;
|
||||
}
|
||||
}
|
||||
// Character skin/face spec-mask map (the cacstamp `specular` channel):
|
||||
// rides the free decal slot like the env families. Written to
|
||||
// overlay.w below: 3 = mask bound, 2 = channel present but not yet
|
||||
// decoded (the PS masks the spec OFF; the DXT1 skin diffuse's opaque
|
||||
// alpha would read as a full-white mask), 0 = no channel (the PS masks
|
||||
// by diffuse alpha^2, cloth/jeans). char_alpha lenses keep the decal
|
||||
// slot for their coverage texture.
|
||||
float char_spec = 0.0f;
|
||||
if ((item.char_family == 1 || item.char_family == 2) && !item.char_alpha &&
|
||||
item.spec_tex != 0) {
|
||||
const GuestTexture* spec = resolve_texture(item.spec_tex, 6);
|
||||
if (spec != &g_r.white && spec->valid && spec->srv_mips != 0) {
|
||||
decal_tex = spec;
|
||||
char_spec = 3.0f;
|
||||
} else {
|
||||
char_spec = 2.0f;
|
||||
}
|
||||
}
|
||||
const bool is_decal =
|
||||
item.char_family < 4 && item.decal && decal_tex != &g_r.white;
|
||||
// Fam 5/6 masks+normal pair (t4/t5): the reflective PS perturbs its
|
||||
|
|
@ -8310,6 +8336,21 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
? 4.0f
|
||||
: 3.0f)
|
||||
: 0.0f);
|
||||
if (char_spec != 0.0f) {
|
||||
// Character fam 1/2 spec-mask state (see the bind above); chars
|
||||
// never take the is_decal/spec_bound paths, so the slot is theirs.
|
||||
constants[47] = char_spec;
|
||||
}
|
||||
if (item.char_family == 1 || item.char_family == 2) {
|
||||
// misc.y = normal/spec-map LOD bias to the console's 640p-gradient
|
||||
// mip (the fam 5/6 cube-bias rationale): at 4K the shader's UV
|
||||
// gradients pick ~1.75 mips finer than the game's own render, and
|
||||
// mip-0 sampling keeps fine wrinkle noise the console filters away
|
||||
// - the authored garment folds read weaker than the emulated
|
||||
// reference without this.
|
||||
constants[49] =
|
||||
log2f(std::max(1.0f, float(context.guest_output_height) / 640.0f));
|
||||
}
|
||||
// Exact flowingwateralpha branch (cam_pos.w = -30): the canal/waterfall
|
||||
// shader hand-ported from the game's own PS and verified per-pixel
|
||||
// against the ucode. Requires the frame m_params rows (b1), the shared
|
||||
|
|
|
|||
|
|
@ -374,7 +374,7 @@ inline std::unordered_map<uint64_t, std::array<uint32_t, 12>> g_frame_draw_fetch
|
|||
// teal-vest twins), so the fallback applies only to meshes with a single
|
||||
// instance in the frame. Guest-render-thread only (capture hooks +
|
||||
// BuildFrameScene), no lock needed.
|
||||
inline std::unordered_map<uint32_t, std::array<float, 60>> g_char_rows_cache;
|
||||
inline std::unordered_map<uint32_t, std::array<float, 72>> g_char_rows_cache;
|
||||
// Frame-global character CSM receive biases (see FrameScene::char_shadow_bias)
|
||||
// + the guest frame they were captured on (served while <120 frames stale;
|
||||
// they only change with the scene lighting setup). Guest render thread only.
|
||||
|
|
@ -413,7 +413,7 @@ inline std::atomic<uint64_t> g_lw_rows_served{0};
|
|||
// legacy flat shading: the "vehicle loses its texture/color at the edge"
|
||||
// sighting.
|
||||
struct CharRowsCtx {
|
||||
std::array<float, 60> rows;
|
||||
std::array<float, 72> rows;
|
||||
uint32_t entity = 0;
|
||||
};
|
||||
inline std::unordered_map<uint32_t, CharRowsCtx> g_char_rows_cache_ctx;
|
||||
|
|
@ -429,7 +429,7 @@ inline std::unordered_map<uint32_t, CharRowsCtx> g_char_rows_cache_ctx;
|
|||
// flicker). Entity-stamped like CharRowsCtx (0 for non-LW instances) so a
|
||||
// recycled ctx never inherits the previous occupant's rows.
|
||||
struct CharRowsInst {
|
||||
std::array<float, 60> rows;
|
||||
std::array<float, 72> rows;
|
||||
uint32_t entity = 0;
|
||||
};
|
||||
inline std::unordered_map<uint64_t, CharRowsInst> g_char_rows_cache_inst;
|
||||
|
|
@ -442,7 +442,7 @@ inline std::unordered_map<uint64_t, CharRowsInst> g_char_rows_cache_inst;
|
|||
// rows across the pair is exact. Garment-to-garment only; body pieces keep
|
||||
// their per-piece caches.
|
||||
struct CharRowsEnt {
|
||||
std::array<float, 60> rows;
|
||||
std::array<float, 72> rows;
|
||||
uint8_t fam = 0; // donor family: the row layout is family-specific
|
||||
};
|
||||
inline std::unordered_map<uint32_t, CharRowsEnt> g_char_rows_cache_ent;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue