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
|
|
@ -211,14 +211,31 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
uint gfxObjId = obj.GfxObjId;
|
||||
float headingDeg = 0f;
|
||||
float transparent = 0f;
|
||||
float luminosity = 1f;
|
||||
// Replace-override luminosity. Stays NaN when there is no
|
||||
// replace entry or none of the keyframe's overrides are set,
|
||||
// and that NaN is the signal to fall back to the surface's
|
||||
// authored Luminosity at draw time. This replaces the previous
|
||||
// `luminosity = 1f` default which masked the surface value
|
||||
// because the `(luminosity > 0) ? luminosity : sub.SurfLuminosity`
|
||||
// fallback at the inner loop never fired (1f is always > 0).
|
||||
// RainMeshProbe (committed b8e0857) confirmed empirically that
|
||||
// NO Dereth sky surface carries the SurfaceType.Luminous flag
|
||||
// bit (0x40) — the differentiator is purely the float field.
|
||||
float replaceLuminosity = float.NaN;
|
||||
if (replaces.TryGetValue((uint)i, out var rep))
|
||||
{
|
||||
if (rep.GfxObjId != 0) gfxObjId = rep.GfxObjId;
|
||||
if (rep.Rotate != 0f) headingDeg = rep.Rotate;
|
||||
transparent = Math.Clamp(rep.Transparent, 0f, 1f);
|
||||
if (rep.Luminosity > 0f) luminosity = rep.Luminosity;
|
||||
if (rep.MaxBright > 0f) luminosity = MathF.Min(luminosity, rep.MaxBright);
|
||||
if (rep.Luminosity > 0f) replaceLuminosity = rep.Luminosity;
|
||||
// MaxBright is a CAP: even if the surface authored Lum=1.0,
|
||||
// a per-keyframe MaxBright trims it. When no explicit
|
||||
// Luminosity replace exists, MaxBright still acts as the
|
||||
// ceiling (applied against sub.SurfLuminosity at draw time).
|
||||
if (rep.MaxBright > 0f)
|
||||
replaceLuminosity = float.IsNaN(replaceLuminosity)
|
||||
? rep.MaxBright
|
||||
: MathF.Min(replaceLuminosity, rep.MaxBright);
|
||||
}
|
||||
if (gfxObjId == 0) continue;
|
||||
|
||||
|
|
@ -262,7 +279,6 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
float vOffset = (obj.TexVelocityY * secondsSinceStart) % 1f;
|
||||
_shader.SetVec2("uUvScroll", new Vector2(uOffset, vOffset));
|
||||
_shader.SetFloat("uTransparency", transparent);
|
||||
_shader.SetFloat("uLuminosity", luminosity);
|
||||
|
||||
EnsureMeshUploaded(gfxObjId);
|
||||
if (!_gpuByGfxObj.TryGetValue(gfxObjId, out var subMeshes)) continue;
|
||||
|
|
@ -281,19 +297,44 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
else
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
||||
|
||||
// Emissive source: retail's FUN_0059da60 for non-luminous
|
||||
// surfaces writes rep.Luminosity into D3DMATERIAL9.Emissive
|
||||
// (via material cache +0x3c). This PROMOTES bright-keyframe
|
||||
// clouds into the self-lit term so the litColor saturates
|
||||
// and the texture renders at full brightness rather than
|
||||
// being dimmed by a per-fragment multiply.
|
||||
// Emissive source picks the surface's authored Luminosity by
|
||||
// default; the per-keyframe replace data can OVERRIDE
|
||||
// (rep.Luminosity > 0) or CAP (rep.MaxBright). This matches
|
||||
// retail's FUN_0059da60: surface.Luminosity → D3DMATERIAL.Emissive
|
||||
// (via material cache +0x3c), with the keyframe replace
|
||||
// promoting bright-keyframe clouds when the keyframe asks.
|
||||
//
|
||||
// If no rep.Luminosity override: fall back to the Surface's
|
||||
// static Luminosity (1.0 for dome/sun/moon → saturates;
|
||||
// 0.0 for stars → stays ambient-lit, correct retail look).
|
||||
float effEmissive = (luminosity > 0f) ? luminosity : sub.SurfLuminosity;
|
||||
// Empirical Dereth sky surfaces (RainMeshProbe, b8e0857):
|
||||
// dome/sun/moon → Lum=1.0 → vTint saturates → texture
|
||||
// passthrough (correct retail look);
|
||||
// stars/clouds → Lum=0.0 → vTint = ambient + diffuse →
|
||||
// picks up the time-of-day tint;
|
||||
// rain → Lum=0.1484 → faint emissive baseline,
|
||||
// ambient+diffuse adds atmospheric tint.
|
||||
//
|
||||
// Pre-fix: the replace-override variable defaulted to 1f and
|
||||
// the fallback `(luminosity > 0) ? luminosity : sub.SurfLuminosity`
|
||||
// never fired — every sky mesh got effEmissive=1.0,
|
||||
// saturating vTint. That made stars/clouds look full-bright
|
||||
// instead of time-of-day-tinted, and made rain streaks
|
||||
// 6.7× too bright (one of two factors compounding the
|
||||
// foreground-rim visibility bug).
|
||||
float effEmissive = float.IsNaN(replaceLuminosity)
|
||||
? sub.SurfLuminosity
|
||||
: replaceLuminosity;
|
||||
_shader.SetFloat("uEmissive", effEmissive);
|
||||
|
||||
// Retail per-Surface translucency override (D3DPolyRender::SetSurface
|
||||
// at 0x59c767): when the Surface's Translucent (0x10) bit is set,
|
||||
// its translucency float drives per-vertex alpha. Both ACViewer
|
||||
// and WorldBuilder render this as opacity = (1 - x). The shader
|
||||
// multiplies output alpha by (1 - uSurfTranslucency); for surfaces
|
||||
// without the bit, SurfTranslucency=0 ⇒ no effect. Critical for
|
||||
// the rain mesh 0x01004C42/0x01004C44 (Translucency=0.5) so its
|
||||
// streaks contribute at half intensity instead of full under the
|
||||
// additive (SrcAlpha, One) blend.
|
||||
_shader.SetFloat("uSurfTranslucency", sub.SurfTranslucency);
|
||||
|
||||
uint tex = _textures.GetOrUpload(sub.SurfaceId);
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
|
|
@ -514,6 +555,7 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
IsAdditive = isAdditive,
|
||||
SurfLuminosity = sm.Luminosity,
|
||||
NeedsUvRepeat = sm.NeedsUvRepeat,
|
||||
SurfTranslucency = sm.SurfTranslucency,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -563,5 +605,16 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
/// Computed once at mesh build from the actual UV range.
|
||||
/// </summary>
|
||||
public bool NeedsUvRepeat;
|
||||
/// <summary>
|
||||
/// <c>Surface.Translucency</c> float (0..1) carried through from
|
||||
/// <see cref="GfxObjSubMesh.SurfTranslucency"/>. Passed to the
|
||||
/// sky fragment shader as <c>uSurfTranslucency</c>; the shader
|
||||
/// multiplies output alpha by <c>(1 - x)</c>. For the rain
|
||||
/// surface 0x080000C5 this is 0.5 → opacity 0.5 → rain streaks
|
||||
/// contribute at half intensity under the additive blend, matching
|
||||
/// retail's <c>curr_alpha</c> derivation in
|
||||
/// <c>D3DPolyRender::SetSurface</c> at <c>0x59c767</c>.
|
||||
/// </summary>
|
||||
public float SurfTranslucency;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue