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:
parent
889b235886
commit
1d54880213
12 changed files with 1217 additions and 43 deletions
175
tools/SkyObjectInspect/Program.cs
Normal file
175
tools/SkyObjectInspect/Program.cs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
// SkyObjectInspect — throwaway probe for the Dereth stars mystery.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Options;
|
||||
using DatReaderWriter.Types;
|
||||
using SysEnv = System.Environment;
|
||||
|
||||
string datDir = SysEnv.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||
?? Path.Combine(SysEnv.GetFolderPath(SysEnv.SpecialFolder.UserProfile),
|
||||
"Documents", "Asheron's Call");
|
||||
|
||||
Console.WriteLine($"datDir = {datDir}");
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
|
||||
if (!dats.TryGet<Region>(0x13000000u, out var region) || region is null)
|
||||
{
|
||||
Console.Error.WriteLine("ERROR: Cannot read Region 0x13000000");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Region loaded. SkyInfo.DayGroups count: {region.SkyInfo?.DayGroups?.Count ?? -1}");
|
||||
|
||||
var interesting = new[] { 0, 8 };
|
||||
foreach (int dg in interesting)
|
||||
{
|
||||
if (region.SkyInfo?.DayGroups is null || dg >= region.SkyInfo.DayGroups.Count) continue;
|
||||
var group = region.SkyInfo.DayGroups[dg];
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"=== DayGroup[{dg}] Name=\"{group.DayName?.Value}\" Chance={group.ChanceOfOccur:F3} SkyObjs={group.SkyObjects.Count} SkyTimes={group.SkyTime.Count} ===");
|
||||
for (int oi = 0; oi < group.SkyObjects.Count; oi++)
|
||||
{
|
||||
var so = group.SkyObjects[oi];
|
||||
Console.WriteLine($" OI={oi}: Begin={so.BeginTime:F3} End={so.EndTime:F3} BeginAng={so.BeginAngle:F1} EndAng={so.EndAngle:F1} TexVel=({so.TexVelocityX:F3},{so.TexVelocityY:F3}) Gfx=0x{(uint)so.DefaultGfxObjectId:X8} Pes=0x{(uint)so.DefaultPesObjectId:X8} Props=0x{so.Properties:X8}");
|
||||
}
|
||||
// Show every SkyTime's SkyObjectReplace entries — this tells us if any OI
|
||||
// actually changes at night.
|
||||
foreach (var skytime in group.SkyTime.OrderBy(s => s.Begin))
|
||||
{
|
||||
Console.WriteLine($" [SkyTime @ Begin={skytime.Begin:F3}] Replaces={skytime.SkyObjReplace.Count}");
|
||||
foreach (var r in skytime.SkyObjReplace)
|
||||
{
|
||||
Console.WriteLine($" OI={r.ObjectIndex}: Gfx=0x{(uint)r.GfxObjId:X8} Rot={r.Rotate:F2} Transp={r.Transparent:F3} Lum={r.Luminosity:F3} MaxB={r.MaxBright:F3}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also scan ALL DayGroups for any SkyObject with BeginTime > EndTime (wrap)
|
||||
// OR BeginTime in late night (>0.75) with a gfx that could be stars.
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("=== Scan: any SkyObject with night-spanning window (begin>0.7 or end<0.3 wrap-candidate) across ALL DayGroups ===");
|
||||
int nFound = 0;
|
||||
if (region.SkyInfo?.DayGroups is not null)
|
||||
{
|
||||
for (int dg = 0; dg < region.SkyInfo.DayGroups.Count; dg++)
|
||||
{
|
||||
var group = region.SkyInfo.DayGroups[dg];
|
||||
for (int oi = 0; oi < group.SkyObjects.Count; oi++)
|
||||
{
|
||||
var so = group.SkyObjects[oi];
|
||||
bool wrap = so.BeginTime > so.EndTime && so.BeginTime != so.EndTime;
|
||||
bool late = so.BeginTime > 0.7f;
|
||||
bool early = so.EndTime < 0.3f && so.EndTime > 0f;
|
||||
if (wrap || late || early)
|
||||
{
|
||||
Console.WriteLine($" DG[{dg}]=\"{group.DayName?.Value}\" OI={oi} Begin={so.BeginTime:F3} End={so.EndTime:F3} Gfx=0x{(uint)so.DefaultGfxObjectId:X8} wrap={wrap} late={late} early={early}");
|
||||
nFound++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Console.WriteLine($" (found {nFound} night-window candidates)");
|
||||
|
||||
// Candidate GfxObjs for Sunny.
|
||||
var candidateIds = new uint[] { 0x010015EEu, 0x010015EFu, 0x01001F6Au, 0x01004C36u, 0x02000714u };
|
||||
foreach (uint gid in candidateIds)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"=== GfxObj 0x{gid:X8} ===");
|
||||
if (gid >= 0x02000000u)
|
||||
{
|
||||
if (dats.TryGet<Setup>(gid, out var setup) && setup is not null)
|
||||
{
|
||||
Console.WriteLine($" [Setup] Parts={setup.Parts.Count}");
|
||||
for (int pi = 0; pi < setup.Parts.Count; pi++)
|
||||
{
|
||||
uint partGid = (uint)setup.Parts[pi];
|
||||
Console.WriteLine($" Part[{pi}] = GfxObj 0x{partGid:X8}");
|
||||
DumpGfxObj(dats, partGid, indent: " ");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(" (not a Setup or not found)");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
DumpGfxObj(dats, gid, indent: " ");
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
static void DumpGfxObj(DatCollection dats, uint gid, string indent)
|
||||
{
|
||||
if (!dats.TryGet<GfxObj>(gid, out var go) || go is null)
|
||||
{
|
||||
Console.WriteLine($"{indent}(GfxObj 0x{gid:X8} not found)");
|
||||
return;
|
||||
}
|
||||
Console.WriteLine($"{indent}GfxObj 0x{gid:X8}: Flags=0x{(uint)go.Flags:X8} Surfaces={go.Surfaces.Count} Polys={go.Polygons.Count} Verts={go.VertexArray?.Vertices?.Count ?? 0}");
|
||||
for (int si = 0; si < go.Surfaces.Count; si++)
|
||||
{
|
||||
uint sid = (uint)go.Surfaces[si];
|
||||
if (!dats.TryGet<Surface>(sid, out var surf) || surf is null)
|
||||
{
|
||||
Console.WriteLine($"{indent} Surf[{si}]=0x{sid:X8} (not found)");
|
||||
continue;
|
||||
}
|
||||
string texDesc = DescribeTexture(dats, surf);
|
||||
Console.WriteLine($"{indent} Surf[{si}]=0x{sid:X8} Type={surf.Type} Translucency={surf.Translucency:F3} Luminosity={surf.Luminosity:F3} Diffuse={surf.Diffuse:F3} Tex=[{texDesc}]");
|
||||
}
|
||||
}
|
||||
|
||||
static string DescribeTexture(DatCollection dats, Surface surf)
|
||||
{
|
||||
if (!(surf.Type.HasFlag(SurfaceType.Base1Image) || surf.Type.HasFlag(SurfaceType.Base1ClipMap)))
|
||||
return $"solid color A=0x{surf.ColorValue.Alpha:X2} R=0x{surf.ColorValue.Red:X2} G=0x{surf.ColorValue.Green:X2} B=0x{surf.ColorValue.Blue:X2}";
|
||||
uint stid = (uint)surf.OrigTextureId;
|
||||
if (stid == 0) return "no-texture";
|
||||
if (!dats.TryGet<SurfaceTexture>(stid, out var st) || st is null)
|
||||
return $"SurfaceTex 0x{stid:X8} missing";
|
||||
if (st.Textures.Count == 0) return $"SurfaceTex 0x{stid:X8} empty";
|
||||
uint rsid = (uint)st.Textures[0];
|
||||
if (!dats.TryGet<RenderSurface>(rsid, out var rs) || rs is null)
|
||||
return $"RenderSurf 0x{rsid:X8} missing";
|
||||
double brightRatio = ApproxBrightRatio(rs);
|
||||
return $"{rs.Width}x{rs.Height} {rs.Format} data={rs.SourceData.Length}B palette=0x{rs.DefaultPaletteId:X8} brightRatio~{brightRatio:F3}";
|
||||
}
|
||||
|
||||
static double ApproxBrightRatio(RenderSurface rs)
|
||||
{
|
||||
if (rs.SourceData is null || rs.SourceData.Length == 0) return 0;
|
||||
if (rs.Format == PixelFormat.PFID_A8R8G8B8)
|
||||
{
|
||||
int bright = 0, total = rs.SourceData.Length / 4;
|
||||
for (int i = 0; i + 4 <= rs.SourceData.Length; i += 4)
|
||||
{
|
||||
byte a = rs.SourceData[i];
|
||||
byte r = rs.SourceData[i + 1];
|
||||
byte g = rs.SourceData[i + 2];
|
||||
byte b = rs.SourceData[i + 3];
|
||||
if (a > 0 && (r + g + b) / 3 > 48) bright++;
|
||||
}
|
||||
return total > 0 ? (double)bright / total : 0;
|
||||
}
|
||||
if (rs.Format == PixelFormat.PFID_R8G8B8)
|
||||
{
|
||||
int bright = 0, total = rs.SourceData.Length / 3;
|
||||
for (int i = 0; i + 3 <= rs.SourceData.Length; i += 3)
|
||||
{
|
||||
byte r = rs.SourceData[i];
|
||||
byte g = rs.SourceData[i + 1];
|
||||
byte b = rs.SourceData[i + 2];
|
||||
if ((r + g + b) / 3 > 48) bright++;
|
||||
}
|
||||
return total > 0 ? (double)bright / total : 0;
|
||||
}
|
||||
int nonZero = 0;
|
||||
for (int i = 0; i < rs.SourceData.Length; i++) if (rs.SourceData[i] != 0) nonZero++;
|
||||
return (double)nonZero / rs.SourceData.Length;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue