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
|
|
@ -200,6 +200,7 @@ public static class GfxObjMesh
|
|||
// docs/research/2026-04-23-sky-retail-verbatim.md §6).
|
||||
var translucency = TranslucencyKind.Opaque;
|
||||
var luminosity = 0f;
|
||||
var surfTranslucency = 0f;
|
||||
if (dats is not null)
|
||||
{
|
||||
var surface = dats.Get<Surface>(surfaceId);
|
||||
|
|
@ -207,6 +208,15 @@ public static class GfxObjMesh
|
|||
{
|
||||
translucency = TranslucencyKindExtensions.FromSurfaceType(surface.Type);
|
||||
luminosity = surface.Luminosity;
|
||||
// Retail D3DPolyRender::SetSurface at 0x59c767: when the
|
||||
// Translucent (0x10) flag is set, the surface's
|
||||
// Translucency float drives per-vertex alpha. Both
|
||||
// ACViewer and WorldBuilder apply opacity = (1 - x).
|
||||
// For the rain Surface 0x080000C5 this is 0.5. Carrying
|
||||
// the float verbatim and converting to opacity in the
|
||||
// shader keeps non-Translucent surfaces (Translucency=0)
|
||||
// identical to the previous behavior.
|
||||
surfTranslucency = surface.Translucency;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -235,6 +245,7 @@ public static class GfxObjMesh
|
|||
Translucency = translucency,
|
||||
Luminosity = luminosity,
|
||||
NeedsUvRepeat = needsUvRepeat,
|
||||
SurfTranslucency = surfTranslucency,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -53,4 +53,22 @@ public sealed record GfxObjSubMesh(
|
|||
/// Defaults to false so non-sky consumers get the previous behavior.
|
||||
/// </summary>
|
||||
public bool NeedsUvRepeat { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// <c>Surface.Translucency</c> float (0..1 — distinct from the
|
||||
/// <see cref="TranslucencyKind"/> classifier above, which buckets the
|
||||
/// flag bits). Retail's <c>D3DPolyRender::SetSurface</c> at
|
||||
/// <c>0x59c767</c> reads this when the <c>Translucent</c> (0x10) bit
|
||||
/// is set on the surface and feeds it into the per-vertex alpha
|
||||
/// (<c>curr_alpha</c>); the rasterizer then multiplies fragment alpha
|
||||
/// by <c>(1 - translucency)</c> so the resulting opacity is
|
||||
/// <c>1 - x</c>. ACViewer (<c>TextureCache.cs:142</c>) and WorldBuilder
|
||||
/// (<c>ObjectMeshManager.cs:1115</c>) both use the same convention.
|
||||
/// For the rain Surface 0x080000C5, <c>Translucency = 0.5</c> ⇒
|
||||
/// opacity = 0.5; with the <c>(SrcAlpha, One)</c> additive blend the
|
||||
/// rain streaks contribute at half intensity instead of full.
|
||||
/// Defaults to 0.0 (fully opaque) so non-translucent surfaces render
|
||||
/// through the normal lighting path without change.
|
||||
/// </summary>
|
||||
public float SurfTranslucency { get; init; } = 0f;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue