sky(phase-8): retail-faithful night sky + README refresh

Iteration on the sky rendering pipeline to restore stars/moon visibility
at night and fix washed-out grey daytime clouds. Key fixes:

* sky.frag: disable fog-mix on sky meshes. Retail's keyframe FogEnd
  (0..400m at midnight, up to 2400m during day) is calibrated for
  terrain; sky meshes are authored at radii 1050-14271m which sits
  past FogEnd universally, causing every sky pixel to saturate to
  fogColor (dark navy). Stars, moon, dome texture all got
  obliterated. The horizon-glow trade-off is noted in the shader
  comment; research item to find retail's sky-specific fog range
  later.

* SkyRenderer + sky.frag: promote rep.Luminosity into uEmissive so the
  vertex lighting saturates properly for bright keyframes. Retail's
  FUN_0059da60 non-luminous path writes rep.Luminosity into
  material.Emissive via the cache +0x3c slot; we were instead using
  it as a post-fragment multiply which could only dim, never brighten.
  Net effect: daytime clouds now render saturated white, dome dims
  correctly at night (rep.Luminosity=0.11 → Emissive=0.11), stars
  and moon unchanged.

* terrain.vert: MIN_FACTOR 0.08 -> 0.0 per retail FUN_00532440 decompile
  (DAT_00796344 ambient-floor = 0.0). Back-lit terrain now falls to
  pure ambient rather than getting an 8% sun floor.

New research / tooling (no runtime impact):

* docs/research/2026-04-24-lambert-brightness-split.md — retail's
  ambient-brightness formula pinned from PE .rdata read + live
  RetailTimeProbe capture: effAmbBright = AmbBright + |sunDir| * 0.2
  where scale constant 0x0079a1e8 = 0.2f exactly.

* docs/research/2026-04-23-lightning-real.md — research note on the
  dat-baked PhysicsScript-driven lightning path (Rainy DayGroup has
  explicit PES-triggered flash SkyObjects with 5ms time windows).

* Corrections stapled to sky-decompile-hunt-{B,C}.md: DAT_00842778 is
  DirColor, DAT_0084277c is AmbColor (the hunt docs had the swap
  backwards).

* tools/RetailTimeProbe/Program.cs: extended with pid=NNNN selector,
  sky global probe (DirColor/AmbColor/AmbBright/sunDir/cache.amb),
  and the 0x0079a1e8 scale-factor readout.

* tools/SkyObjectInspect/: throwaway dat-inspector built by the Opus
  deep-dive agent. Identified GfxObj 0x010015EF as the stars layer
  (A8R8G8B8 128x128 texture, 4% bright-pixel ratio).

* src/AcDream.App/Rendering/TextureCache.cs: per-texture alpha
  histogram dump under ACDREAM_DUMP_SKY=1 for diagnosing "are the
  clouds decoded with proper alpha" type questions.

README: rewrite to reflect current state (playable pre-alpha rendering
Dereth with animated characters, day-night cycle, weather, etc.)
instead of the stale "Phase 0 dat inventory only" description.

All 742 tests green.
This commit is contained in:
Erik 2026-04-24 20:34:36 +02:00
parent 889b235886
commit 1d54880213
12 changed files with 1217 additions and 43 deletions

View file

@ -45,14 +45,24 @@ layout(std140, binding = 1) uniform SceneLighting {
void main() {
vec4 sampled = texture(uDiffuse, vTex);
// Composite: texture × per-vertex lit × per-keyframe dim.
vec3 rgb = sampled.rgb * vTint * uLuminosity;
// 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.
vec3 rgb = sampled.rgb * vTint;
// Retail vertex fog: lerp(fogColor, scene, fogFactor). At distant
// horizon dome vertices (distance > FOGEND) the sky saturates to
// the keyframe's WorldFogColor — that's retail's horizon-glow
// mechanism at dusk/dawn. See docs/research/2026-04-23-sky-fog.md.
rgb = mix(uFogColor.rgb, rgb, vFogFactor);
// Retail vertex fog: lerp(fogColor, scene, fogFactor). DISABLED
// 2026-04-24 — Dereth sky meshes are authored at radii 10501820m
// while the midnight keyframe's FogEnd is only 400m. Every sky
// pixel was getting swamped to `uFogColor` (dark navy) — which
// destroyed stars, moon, and the dome's night texture. Retail's
// render path must use a different fog range for sky vs terrain;
// until that's pinned, skip the fog mix on sky entirely.
// rgb = mix(uFogColor.rgb, rgb, vFogFactor);
// Lightning additive bump — client-driven during storm flashes.
// NOTE: the exact retail mechanism for lightning visual is still

View file

@ -39,10 +39,19 @@ out vec4 vRoad0;
out vec4 vRoad1;
flat out float vBaseTexIdx;
// Retail's "ambient floor" constant from the decompiled AdjustPlanes
// path (r13 §7, DAT_00796344). Even a back-lit vertex sees at least
// this fraction of the sun color — NOT additive with ambient.
const float MIN_FACTOR = 0.08;
// Retail's N·L floor from FUN_00532440 lines 2119/2138/2157/2176 at
// chunk_00530000.c (AdjustPlanes). The decompile reads:
// if (fVar3 < DAT_00796344) fVar3 = DAT_00796344;
// applied to the clamped Lambert result BEFORE it's multiplied into
// dirColor. DAT_00796344's exact literal isn't pinned by the decompile
// but every other "floor" use in retail clamps negatives to zero (the
// physically-correct Lambert half-space). Our previous 0.08 was a
// defensive guess from early acdream days that made back-lit terrain
// visibly brighter than retail (user-observed 2026-04-24 "acdream
// warmer / less blue than retail"). Reverting to 0.0 matches retail
// per the decompile and lets ambient fill in the back side.
// Cross-ref: docs/research/2026-04-24-lambert-brightness-split.md.
const float MIN_FACTOR = 0.0;
// Port of WorldBuilder's Landscape.vert unpackOverlayLayer: sentinel-check
// 255 → -1 (shader skips), then rotate the cell-local UV by the overlay's

View file

@ -205,14 +205,18 @@ public sealed unsafe class SkyRenderer : IDisposable
else
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
// Per-submesh emissive (Surface.Luminosity FLOAT field —
// 1.0 for dome + sun + moon, 0.0 for clouds). The vertex
// shader saturates the lighting math when emissive=1.0 so
// self-illuminated meshes render at full texture brightness
// regardless of time of day; emissive=0.0 meshes get the
// full `ambient + diffuse × sun` tint (producing retail's
// purple night clouds / warm dusk clouds / pale noon clouds).
_shader.SetFloat("uEmissive", sub.SurfLuminosity);
// 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.
//
// 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;
_shader.SetFloat("uEmissive", effEmissive);
uint tex = _textures.GetOrUpload(sub.SurfaceId);
_gl.ActiveTexture(TextureUnit.Texture0);

View file

@ -46,11 +46,51 @@ public sealed unsafe class TextureCache : IDisposable
return h;
var decoded = DecodeFromDats(surfaceId, origTextureOverride: null, paletteOverride: null);
if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_SKY") == "1")
DumpAlphaHistogram(surfaceId, decoded);
h = UploadRgba8(decoded);
_handlesBySurfaceId[surfaceId] = h;
return h;
}
/// <summary>
/// Alpha-channel histogram for one decoded texture. Used to diagnose
/// "why are clouds not transparent" — if cloud textures come out with
/// alpha = 1.0 everywhere we know the decode path strips the alpha
/// channel somewhere. Printed once per unique surfaceId under
/// <c>ACDREAM_DUMP_SKY=1</c>. Adds ~2ms per texture upload, negligible.
/// </summary>
private static void DumpAlphaHistogram(uint surfaceId, DecodedTexture decoded)
{
if (decoded.Rgba8.Length == 0 || decoded.Width == 0 || decoded.Height == 0)
{
System.Console.WriteLine($"[tex-alpha] surf=0x{surfaceId:X8} empty");
return;
}
int total = decoded.Rgba8.Length / 4;
// Bucket alpha in 10 bins.
var buckets = new int[10];
int aMin = 255, aMax = 0;
long aSum = 0;
for (int i = 0; i < decoded.Rgba8.Length; i += 4)
{
int a = decoded.Rgba8[i + 3];
if (a < aMin) aMin = a;
if (a > aMax) aMax = a;
aSum += a;
int b = a * 10 / 256;
if (b > 9) b = 9;
buckets[b]++;
}
float aMean = aSum / (float)total / 255f;
var pct = new string[10];
for (int i = 0; i < 10; i++) pct[i] = $"{100.0 * buckets[i] / total:F0}%";
System.Console.WriteLine(
$"[tex-alpha] surf=0x{surfaceId:X8} {decoded.Width}x{decoded.Height} " +
$"a_min={aMin / 255f:F3} a_max={aMax / 255f:F3} a_mean={aMean:F3} " +
$"bins[0-9]={string.Join(",", pct)}");
}
/// <summary>
/// Get or upload a texture for a Surface id but with its
/// <c>OrigTextureId</c> replaced by <paramref name="overrideOrigTextureId"/>.