fix(sky): apply per-Surface Translucency + Luminosity for retail-faithful weather
Two independent brightness bugs were compounding to make rain ~6.7×
too bright at the cylinder rim, and clouds full-bright instead of
time-of-day-tinted:
**Fix 1 — Surface.Translucency was never plumbed to the shader.**
Retail's D3DPolyRender::SetSurface at 0x59c767: when the Surface's
Translucent (0x10) bit is set, its translucency float drives per-vertex
alpha (curr_alpha = ftol(0.5 × 255) = 127). ACViewer
(TextureCache.cs:142) and WorldBuilder (ObjectMeshManager.cs:1115) both
encode the same as `opacity = (1 - x)`. acdream read only Surface.Type
and Surface.Luminosity in GfxObjMesh.Build() — Surface.Translucency
(the float) was never read, never stored, never reached the shader.
For the rain Surface 0x080000C5 (Translucency=0.5) this meant rain
streaks were at full alpha=1.0 instead of 0.5 — 2× brighter than retail
under the (SrcAlpha, One) blend.
Plumbed end-to-end:
GfxObjSubMesh.SurfTranslucency (init float, default 0)
GfxObjMesh.Build() reads surface.Translucency next to .Luminosity
SubMeshGpu.SurfTranslucency carries it to draw time
SkyRenderer.RenderPass writes uniform `uSurfTranslucency`
sky.frag final alpha: a = sampled.a × (1 - uTransparency) ×
(1 - uSurfTranslucency)
Bonus reach: cloud surface 0x08000023 has Translucency=0.25 → clouds
also dimmed by 25%, more retail-faithful overall.
**Fix 2 — Emissive default was 1.0 instead of the surface's actual Luminosity.**
The sky shader's `effEmissive = (luminosity > 0) ? luminosity : sub.SurfLuminosity`
fallback never fired because the local `luminosity` defaulted to 1f (always
> 0). Every sky mesh got effEmissive=1.0, saturating vTint to white before
the alpha blend. The comment claimed the fallback was active; the code
disagreed.
Empirical sky-surface LUMINOUS audit (RainMeshProbe a6e7108) found that
NO Dereth sky surface carries the SurfaceType.Luminous flag (0x40) —
the previous code comment that did was wrong. The differentiator is
purely the Surface.Luminosity FLOAT:
dome/sun/moon: Lum=1.0 → vTint saturates → texture passthrough
stars/clouds: Lum=0.0 → vTint = ambient + sun·N·L → time-of-day tint
rain: Lum=0.1484 → faint emissive baseline + lit additions
Refactored:
replaceLuminosity = NaN sentinel for "no replace override"
rep.Luminosity > 0 → set replaceLuminosity to override value
rep.MaxBright > 0 → cap replaceLuminosity at MaxBright
effEmissive = NaN ? sub.SurfLuminosity : replaceLuminosity
Dead uniform `uLuminosity` removed from sky.frag and SkyRenderer SetFloat
call — the redundant multiply was already commented-out earlier this
year (would have double-dimmed clouds), and the uniform value was unused
in the fragment.
Visual verification (Holtburg, live ACE, Rainy DG forced and natural
LCG-picked): rain rim is no longer visible; cloud direction matches
retail when the same DayGroup is active; sky lighting transitions through
day cycle with appropriate time-of-day tint on stars/clouds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a6e7108122
commit
4678b3ee6b
4 changed files with 127 additions and 33 deletions
|
|
@ -2,17 +2,16 @@
|
|||
// Sky mesh fragment shader — final composite matching retail's
|
||||
// D3D fixed-function:
|
||||
//
|
||||
// fragment.rgb = texture.rgb × vTint × uLuminosity + lightning_flash
|
||||
// fragment.a = texture.a × (1 - uTransparency)
|
||||
// fragment.rgb = texture.rgb × vTint + lightning_flash
|
||||
// fragment.a = texture.a × (1 - uTransparency) × (1 - uSurfTranslucency)
|
||||
//
|
||||
// vTint arrives from the vertex shader with retail's per-vertex
|
||||
// lighting formula baked in (Emissive + lightAmbient + lightDiffuse ×
|
||||
// max(N·L, 0)) — see sky.vert for the decompile citation.
|
||||
//
|
||||
// uLuminosity is the per-keyframe SkyObjectReplace.Luminosity override
|
||||
// (0..1, /100 in SkyDescLoader). It's a SEPARATE field from the
|
||||
// Surface.Luminosity that feeds uEmissive in the vertex shader — they
|
||||
// compose multiplicatively in retail too.
|
||||
// max(N·L, 0)) — see sky.vert for the decompile citation. The keyframe
|
||||
// SkyObjectReplace.Luminosity override is folded into uEmissive on the
|
||||
// CPU side (SkyRenderer.cs) so vTint already saturates properly for
|
||||
// bright keyframes; the previous shader had a redundant uLuminosity
|
||||
// multiply that was double-dimming clouds, removed 2026-04-26.
|
||||
//
|
||||
// See `docs/research/2026-04-23-sky-material-state.md`.
|
||||
|
||||
|
|
@ -22,8 +21,15 @@ in float vFogFactor; // 1 = no fog (near), 0 = full fog color (far)
|
|||
out vec4 fragColor;
|
||||
|
||||
uniform sampler2D uDiffuse;
|
||||
uniform float uTransparency; // 0 = fully visible, 1 = fully transparent
|
||||
uniform float uLuminosity; // SkyObjectReplace.Luminosity override (0..1)
|
||||
uniform float uTransparency; // 0 = fully visible, 1 = fully transparent
|
||||
// Surface.Translucency float (0..1) — distinct from uTransparency
|
||||
// (which is the per-keyframe Replace override). Retail
|
||||
// D3DPolyRender::SetSurface at 0x59c767 reads this when the Surface's
|
||||
// Translucent (0x10) bit is set and converts to per-vertex alpha;
|
||||
// ACViewer + WorldBuilder both apply opacity = (1 - x). Both factors
|
||||
// compose multiplicatively into final fragment alpha. For non-Translucent
|
||||
// surfaces uSurfTranslucency = 0 ⇒ no effect.
|
||||
uniform float uSurfTranslucency;
|
||||
|
||||
// Shared SceneLighting UBO — fog params drive the mix, flash channel
|
||||
// bumps sky brightness during lightning strikes. Matches sky.vert's
|
||||
|
|
@ -45,14 +51,13 @@ layout(std140, binding = 1) uniform SceneLighting {
|
|||
void main() {
|
||||
vec4 sampled = texture(uDiffuse, vTex);
|
||||
|
||||
// Composite: texture × per-vertex lit.
|
||||
// `rep.Luminosity` is now pushed into `uEmissive` on the CPU side
|
||||
// (SkyRenderer.cs) so `vTint` already saturates properly for bright
|
||||
// keyframes. Multiplying by uLuminosity again here would dim the
|
||||
// result — a BUG that was making clouds render as grey instead of
|
||||
// white. Retail's fragment formula (FUN_0059da60 non-luminous
|
||||
// branch) is texture × litColor × vertex.color(=white), so just
|
||||
// `texture × vTint` is the retail-faithful composite.
|
||||
// Composite: texture × per-vertex lit. Replace.Luminosity (per
|
||||
// keyframe) and Surface.Luminosity are both folded into uEmissive
|
||||
// on the CPU side (SkyRenderer.cs) so vTint already carries the
|
||||
// right tint for the time-of-day. Retail's fragment formula
|
||||
// (FUN_0059da60 non-luminous branch) is texture × litColor ×
|
||||
// vertex.color(=white), so `texture × vTint` is the retail-faithful
|
||||
// composite.
|
||||
vec3 rgb = sampled.rgb * vTint;
|
||||
|
||||
// Retail vertex fog: lerp(fogColor, scene, fogFactor). DISABLED
|
||||
|
|
@ -79,7 +84,14 @@ void main() {
|
|||
float cap = mix(1.0, 3.0, clamp(flash, 0.0, 1.0));
|
||||
rgb = min(rgb, vec3(cap));
|
||||
|
||||
float a = sampled.a * (1.0 - uTransparency);
|
||||
// Final fragment alpha = texture-alpha × keyframe-replace-opacity
|
||||
// × surface-translucency-opacity. Both opacity factors are
|
||||
// (1 - x) form per ACViewer (TextureCache.cs:142) and WorldBuilder
|
||||
// (ObjectMeshManager.cs:1115). For the rain mesh 0x01004C42/0x01004C44:
|
||||
// sampled.a = 1.0 (R8G8B8 texture), uTransparency = 0,
|
||||
// uSurfTranslucency = 0.5 → a = 0.5 → with the (SrcAlpha, One) blend
|
||||
// the streak contribution is halved, matching retail's curr_alpha = 127.
|
||||
float a = sampled.a * (1.0 - uTransparency) * (1.0 - uSurfTranslucency);
|
||||
if (a < 0.01) discard;
|
||||
fragColor = vec4(rgb, a);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue