sky(phase-1): revert speculative tint, add ACDREAM_DUMP_SKY diagnostic
The uncommitted uTint=AmbientColor-for-alpha-submeshes experiment (from
the 2026-04-22 inference) dimmed the sky dome's baked gradient — a
user-verified visual regression. Reverting to the eeae83a baseline
(uTint=Vector4.One for every submesh) while we execute the proper
retail-verbatim port.
Research: three parallel decompile-hunt agents landed verifying
retail's ground-truth sky pipeline for the first time (prior audits
searched for stripped symbol names; the trail opened via the Region
dat-type-index 0x1c registration at chunk_00410000.c:12952). Key
retail functions now mapped in chunk_00500000.c:1097-7535:
- FUN_00501530: keyframe bracket-picker (with 1.0f wrap denominator)
- FUN_00501600: sun+ambient interpolator (sunVec = DirBright ×
(sin yaw·cos pit, cos yaw·cos pit, sin pit))
- FUN_00501860: fog interpolator
- FUN_00502820: SkyDesc::Unpack (2 doubles + DayGroup list)
- FUN_00502a10: build per-frame sky-object table
- FUN_00505f30: apply light state + per-cell AdjustPlanes relight
- FUN_005062e0: per-frame sky tick (throttled by LightTickSize)
- FUN_00508010: sky-object render loop (enqueues through the NORMAL
mesh pipeline via FUN_00514b90 — not a bespoke path)
Surprise findings:
- D3DRS_AMBIENT is set to 0 once at init and NEVER changes per-frame
(chunk_005A0000.c). The r12-inferred "clouds = texture × D3DRS_
AMBIENT" formula is falsified. Retail instead routes keyframe
AmbColor through per-vertex lighting on non-Luminous sky meshes
via _DAT_008682bc/c0/c4.
- Retail does NOT anchor the sky to the camera or use a separate
sky projection. Sky meshes live in world space and follow the
camera via scene-graph parent.
- FUN_00532440 (AdjustPlanes) re-lights every terrain cell on every
keyframe tick — the "terrain follows the sky" effect we don't yet
reproduce.
Phase 1 code change (this commit):
- src/AcDream.App/Rendering/Sky/SkyRenderer.cs: revert uTint to white
for all submeshes (the per-submesh blend split stays — sun gets
additive, clouds get alpha). Keep the `keyframe` parameter in the
signature for Phase 2 readiness. Comments now cite the retail
functions and reference docs instead of the (disproven) r12 formula.
- src/AcDream.Core/World/SkyDescLoader.cs: ACDREAM_DUMP_SKY=1 logs
the entire Region SkyDesc on load — DayGroups, SkyObjects, every
SkyTimeOfDay keyframe, and every SkyObjectReplace with RAW pre-/100
Transparent/Luminosity/MaxBright values so we can settle the unit
question empirically.
- src/AcDream.App/Rendering/Sky/SkyRenderer.cs: ACDREAM_DUMP_SKY=1
additionally logs each sky GfxObj's Surfaces and their SurfaceType
flags on first load, so we can identify which meshes carry the
Luminous bit (dome? sun? moon? stars?) vs which are lit.
- src/AcDream.App/Rendering/GameWindow.cs: passes the interpolated
keyframe to the sky renderer (kept — needed for Phase 2).
Research docs (pushed as part of this commit):
- docs/research/2026-04-23-sky-retail-verbatim.md: full synthesis
with retail function map, struct layouts, globals, pseudocode, and
a 4-phase port plan.
- docs/research/2026-04-23-sky-decompile-hunt-{A,B,C}.md: raw hunt
outputs.
- docs/research/2026-04-23-sky-references-crossref.md: WorldBuilder/
ACE/ACViewer/holtburger/Chorizite coverage.
- docs/research/2026-04-23-sky-dat-schema.md: full dat schema + unit
analysis.
- docs/research/2026-04-22-sky-lighting-decompile.md: prior agent's
(superseded) inference — kept for provenance.
Phase 2 will port Surface.Luminous-flag-aware per-vertex lighting for
sky submeshes once the dump resolves the open questions (Luminous-flag
distribution per Dereth sky mesh; _DAT_007a1870 scale constant value).
Build + 717 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
eeae83a14e
commit
58afd4850f
10 changed files with 2854 additions and 10 deletions
|
|
@ -3612,7 +3612,7 @@ public sealed class GameWindow : IDisposable
|
|||
if (!cameraInsideCell)
|
||||
{
|
||||
_skyRenderer?.Render(camera, camPos, (float)WorldTime.DayFraction,
|
||||
_loadedSkyDesc?.DefaultDayGroup);
|
||||
_loadedSkyDesc?.DefaultDayGroup, kf);
|
||||
}
|
||||
|
||||
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
|
||||
|
|
|
|||
|
|
@ -72,12 +72,28 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
/// <summary>
|
||||
/// Draw the sky for this frame. Called FIRST in the render loop —
|
||||
/// terrain / meshes / debug lines / overlay land on top.
|
||||
///
|
||||
/// <para>
|
||||
/// <paramref name="keyframe"/> is accepted for forward-compatibility
|
||||
/// with the retail-verbatim per-vertex lighting path (see
|
||||
/// <c>docs/research/2026-04-23-sky-retail-verbatim.md</c>). It is
|
||||
/// NOT currently consumed by the shader — sky meshes render at
|
||||
/// <c>uTint = white</c> (texture passthrough). A prior experiment
|
||||
/// multiplied alpha-blended submeshes by <c>keyframe.AmbientColor</c>
|
||||
/// to tint clouds; this dimmed the sky dome's baked gradient
|
||||
/// (user-verified regression) and was reverted. Retail actually
|
||||
/// routes sky meshes through the normal mesh pipeline with
|
||||
/// Surface.Type.Luminous controlling lit-vs-unlit per submesh; the
|
||||
/// correct port lives downstream in Phase 2 once we have the live
|
||||
/// Surface flags dumped.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void Render(
|
||||
ICamera camera,
|
||||
Vector3 cameraWorldPos,
|
||||
float dayFraction,
|
||||
DayGroupData? group)
|
||||
DayGroupData? group,
|
||||
SkyKeyframe keyframe)
|
||||
{
|
||||
if (group is null || group.SkyObjects.Count == 0) return;
|
||||
|
||||
|
|
@ -167,6 +183,16 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
_shader.SetVec2("uUvScroll", new Vector2(uOffset, vOffset));
|
||||
_shader.SetFloat("uTransparency", transparent);
|
||||
_shader.SetFloat("uLuminosity", luminosity);
|
||||
// uTint stays white: retail renders sky meshes as texture
|
||||
// passthrough (the gradient lives in the mesh texture, not in
|
||||
// a shader ambient multiply). D3DRS_AMBIENT is set to 0 once
|
||||
// at retail device-init and never changes per-frame — verified
|
||||
// in chunk_005A0000.c (state 0x8b = 139, only external caller
|
||||
// is the default-reset at line 704). The "cloud tint" effect
|
||||
// comes from per-vertex lighting on non-Luminous submeshes
|
||||
// routed through the normal mesh pipeline. That path is
|
||||
// Phase 2 — see docs/research/2026-04-23-sky-retail-verbatim.md
|
||||
// §6 + §10 and the hunt-B finding at 2026-04-23-sky-decompile-hunt-B.md.
|
||||
_shader.SetVec4("uTint", Vector4.One);
|
||||
|
||||
EnsureMeshUploaded(gfxObjId);
|
||||
|
|
@ -174,16 +200,21 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
|
||||
foreach (var sub in subMeshes)
|
||||
{
|
||||
// Per-submesh blend mode: sun/moon/stars are usually
|
||||
// Additive or Luminous, clouds are AlphaBlend, star dome
|
||||
// backing is Opaque (but we still need blend-enabled to
|
||||
// avoid a hard seam against the sky gradient behind it —
|
||||
// we map Opaque to a passthrough SrcAlpha/OneMinusSrcAlpha
|
||||
// with alpha=1, which is equivalent to not blending).
|
||||
// Per-submesh blend mode: sun/moon/stars are Additive
|
||||
// (SurfaceType.Additive = 0x10000), clouds are AlphaBlend,
|
||||
// sky dome is either Opaque or AlphaBlend depending on the
|
||||
// dat. We map Opaque to a passthrough SrcAlpha/InvSrcAlpha
|
||||
// with alpha=1, which is equivalent to not blending. This
|
||||
// split is architecturally correct (sun's additive blend
|
||||
// stops its black texture background from occluding the
|
||||
// sky dome behind it) but is NOT how retail does it —
|
||||
// retail routes sky meshes through the normal mesh pipe
|
||||
// where Surface flags dictate blend state per primitive.
|
||||
// See FUN_00508010 (chunk_00500000.c:7535).
|
||||
if (sub.IsAdditive)
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One); // additive
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
|
||||
else
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); // alpha
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
||||
|
||||
uint tex = _textures.GetOrUpload(sub.SurfaceId);
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
|
|
@ -267,12 +298,63 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
// Phase 1 diagnostic: dump Surface.Type flags on every sky GfxObj
|
||||
// once, so we can determine which submeshes carry Luminous (0x40)
|
||||
// vs plain-lit. This settles the retail "cloud tint = per-vertex
|
||||
// lighting on non-Luminous meshes" hypothesis — see
|
||||
// docs/research/2026-04-23-sky-retail-verbatim.md §6.
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_SKY") == "1")
|
||||
DumpGfxObjSurfaces(gfxObjId, gfx, subMeshes);
|
||||
|
||||
var gpuList = new List<SubMeshGpu>(subMeshes.Count);
|
||||
foreach (var sm in subMeshes)
|
||||
gpuList.Add(UploadSubMesh(sm));
|
||||
_gpuByGfxObj[gfxObjId] = gpuList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log each surface's raw flag bits and the derived
|
||||
/// <see cref="TranslucencyKind"/>. Called once per GfxObj when
|
||||
/// <c>ACDREAM_DUMP_SKY=1</c>. Output format is grep-friendly so
|
||||
/// we can pipe the launch log through <c>| grep sky-dump</c> and
|
||||
/// recover a complete picture of the Dereth sky without re-running.
|
||||
/// </summary>
|
||||
private void DumpGfxObjSurfaces(
|
||||
uint gfxObjId,
|
||||
GfxObj gfx,
|
||||
System.Collections.Generic.IReadOnlyList<GfxObjSubMesh> subMeshes)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[sky-dump] GfxObj 0x{gfxObjId:X8} Surfaces.Count={gfx.Surfaces.Count} Polygons.Count={gfx.Polygons.Count} SubMeshes.Count={subMeshes.Count}");
|
||||
|
||||
for (int i = 0; i < gfx.Surfaces.Count; i++)
|
||||
{
|
||||
uint surfaceId = (uint)gfx.Surfaces[i];
|
||||
DatReaderWriter.DBObjs.Surface? surface = null;
|
||||
try { surface = _dats.Get<DatReaderWriter.DBObjs.Surface>(surfaceId); }
|
||||
catch { surface = null; }
|
||||
|
||||
if (surface is null)
|
||||
{
|
||||
Console.WriteLine($"[sky-dump] Surface[{i}] 0x{surfaceId:X8} -- (dat read failed)");
|
||||
continue;
|
||||
}
|
||||
|
||||
// SurfaceType is a flag enum — `ToString()` gives the
|
||||
// comma-joined names (e.g. "Base1Image, Additive").
|
||||
uint rawType = (uint)surface.Type;
|
||||
string names = surface.Type.ToString();
|
||||
uint origTex = surface.OrigTextureId?.DataId ?? 0u;
|
||||
var trans = TranslucencyKindExtensions.FromSurfaceType(surface.Type);
|
||||
// Surface's own Luminosity (0..1 fraction per test fixture —
|
||||
// different from SkyObjectReplace.Luminosity which lives in the keyframe).
|
||||
Console.WriteLine(
|
||||
$"[sky-dump] Surface[{i}] 0x{surfaceId:X8} Type=0x{rawType:X8} ({names}) " +
|
||||
$"OrigTexture=0x{origTex:X8} Translucency={trans} " +
|
||||
$"SurfLuminosity={surface.Luminosity:F4} SurfTranslucency={surface.Translucency:F4}");
|
||||
}
|
||||
}
|
||||
|
||||
private SubMeshGpu UploadSubMesh(GfxObjSubMesh sm)
|
||||
{
|
||||
uint vao = _gl.GenVertexArray();
|
||||
|
|
|
|||
|
|
@ -200,6 +200,16 @@ public static class SkyDescLoader
|
|||
/// Convert an in-memory Region object to our domain data.
|
||||
/// Separated so tests can feed hand-built Regions without the dat
|
||||
/// pipeline.
|
||||
///
|
||||
/// <para>
|
||||
/// Set <c>ACDREAM_DUMP_SKY=1</c> in the environment to log the
|
||||
/// entire decoded SkyDesc (raw dat values, pre-/100 divide) to
|
||||
/// stdout on load. Paired with the decompile research in
|
||||
/// <c>docs/research/2026-04-23-sky-retail-verbatim.md</c> — the
|
||||
/// dump resolves the open questions about Transparent/Luminosity/
|
||||
/// MaxBright unit (percent vs fraction) and the per-keyframe
|
||||
/// GfxObjReplace swap pattern.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static LoadedSkyDesc? LoadFromRegion(Region region)
|
||||
{
|
||||
|
|
@ -208,6 +218,10 @@ public static class SkyDescLoader
|
|||
return null;
|
||||
|
||||
var sky = region.SkyInfo;
|
||||
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_SKY") == "1")
|
||||
DumpRegionSkyDesc(region);
|
||||
|
||||
var dayGroups = new List<DayGroupData>(sky.DayGroups.Count);
|
||||
|
||||
foreach (var dg in sky.DayGroups)
|
||||
|
|
@ -232,6 +246,80 @@ public static class SkyDescLoader
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One-shot diagnostic dump of the retail Region's SkyDesc. Prints
|
||||
/// every DayGroup, SkyObject, SkyTimeOfDay, and SkyObjectReplace
|
||||
/// with RAW dat values (before any unit transform) so we can compare
|
||||
/// against the retail decompile field layouts and resolve:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>The unit of <c>Transparent</c>/<c>Luminosity</c>/<c>MaxBright</c>
|
||||
/// (if consistently >1, they're percent — our <c>/100</c> divide is correct;
|
||||
/// if consistently in [0,1], they're fractions and the divide is wrong).</description></item>
|
||||
/// <item><description>Which DayGroup keyframes actually swap the
|
||||
/// GfxObj (non-zero <c>GfxObjId</c>) and which just tweak brightness.</description></item>
|
||||
/// <item><description>The full Dereth sky-object inventory —
|
||||
/// index-to-role mapping (sun/moon/dome/clouds/stars).</description></item>
|
||||
/// </list>
|
||||
/// Logs to stdout with the prefix <c>[sky-dump]</c>. Gate with
|
||||
/// <c>ACDREAM_DUMP_SKY=1</c>.
|
||||
/// </summary>
|
||||
private static void DumpRegionSkyDesc(Region region)
|
||||
{
|
||||
var sky = region.SkyInfo;
|
||||
if (sky is null) return;
|
||||
|
||||
Console.WriteLine("[sky-dump] ======== BEGIN SkyDesc dump ========");
|
||||
Console.WriteLine($"[sky-dump] Region Id={region.Id:X8} Number={region.RegionNumber} Name=\"{region.RegionName}\"");
|
||||
Console.WriteLine($"[sky-dump] SkyDesc TickSize={sky.TickSize} LightTickSize={sky.LightTickSize} DayGroups.Count={sky.DayGroups.Count}");
|
||||
|
||||
for (int g = 0; g < sky.DayGroups.Count; g++)
|
||||
{
|
||||
var dg = sky.DayGroups[g];
|
||||
Console.WriteLine($"[sky-dump] DayGroup[{g}] Name=\"{dg.DayName}\" Chance={dg.ChanceOfOccur:F3} SkyObjects.Count={dg.SkyObjects.Count} SkyTime.Count={dg.SkyTime.Count}");
|
||||
|
||||
for (int i = 0; i < dg.SkyObjects.Count; i++)
|
||||
{
|
||||
var o = dg.SkyObjects[i];
|
||||
uint gfxId = o.DefaultGfxObjectId?.DataId ?? 0u;
|
||||
uint pesId = o.DefaultPesObjectId?.DataId ?? 0u;
|
||||
Console.WriteLine(
|
||||
$"[sky-dump] SkyObject[{i}] GfxObjId=0x{gfxId:X8} PesObjectId=0x{pesId:X8} " +
|
||||
$"Time=[{o.BeginTime:F4}..{o.EndTime:F4}] Angle=[{o.BeginAngle:F1}°..{o.EndAngle:F1}°] " +
|
||||
$"TexVel=({o.TexVelocityX:F5},{o.TexVelocityY:F5}) Properties=0x{o.Properties:X8}");
|
||||
}
|
||||
|
||||
for (int k = 0; k < dg.SkyTime.Count; k++)
|
||||
{
|
||||
var t = dg.SkyTime[k];
|
||||
string dirColor = t.DirColor is null ? "null" :
|
||||
$"({t.DirColor.Red},{t.DirColor.Green},{t.DirColor.Blue},{t.DirColor.Alpha})";
|
||||
string ambColor = t.AmbColor is null ? "null" :
|
||||
$"({t.AmbColor.Red},{t.AmbColor.Green},{t.AmbColor.Blue},{t.AmbColor.Alpha})";
|
||||
string fogColor = t.WorldFogColor is null ? "null" :
|
||||
$"({t.WorldFogColor.Red},{t.WorldFogColor.Green},{t.WorldFogColor.Blue},{t.WorldFogColor.Alpha})";
|
||||
Console.WriteLine(
|
||||
$"[sky-dump] SkyTime[{k}] Begin={t.Begin:F4} " +
|
||||
$"DirBright={t.DirBright:F4} DirHeading={t.DirHeading:F1}° DirPitch={t.DirPitch:F1}° " +
|
||||
$"DirColor={dirColor} AmbBright={t.AmbBright:F4} AmbColor={ambColor} " +
|
||||
$"Fog=[{t.MinWorldFog:F1}m..{t.MaxWorldFog:F1}m] FogColor={fogColor} FogMode={t.WorldFog}");
|
||||
|
||||
for (int r = 0; r < t.SkyObjReplace.Count; r++)
|
||||
{
|
||||
var rep = t.SkyObjReplace[r];
|
||||
uint rGfx = rep.GfxObjId?.DataId ?? 0u;
|
||||
// RAW values — pre-/100 divide. Compare these against the retail
|
||||
// scale constant _DAT_007a1870 to settle the unit question.
|
||||
Console.WriteLine(
|
||||
$"[sky-dump] Replace[{r}] ObjectIndex={rep.ObjectIndex} GfxObjId=0x{rGfx:X8} " +
|
||||
$"Rotate={rep.Rotate:F3}° Transparent_raw={rep.Transparent:F6} " +
|
||||
$"Luminosity_raw={rep.Luminosity:F6} MaxBright_raw={rep.MaxBright:F6}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("[sky-dump] ======== END SkyDesc dump ========");
|
||||
}
|
||||
|
||||
private static SkyObjectData ConvertSkyObject(SkyObject s) => new()
|
||||
{
|
||||
BeginTime = s.BeginTime,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue