acdream/docs/research/2026-08-22-dereth-celestial-shadow-sources.md

200 lines
9.3 KiB
Markdown

# Dereth celestial shadow sources
**Date:** 2026-08-22
**Status:** measured retail-DAT and named-retail finding; implementation input
for Campaign AR
**Scope:** identify the Dereth sun/moons and define the opt-in pack's dominant
directional-shadow source. This note does not change the retail rendering path.
## Conclusion
Dereth's Region `0x13000000` consistently authors three moving celestial
meshes across all 20 day groups:
1. `0x01001348` is the sun disk.
2. `0x01001F6A` is the large, haloed moon and is the dominant lunar source.
3. `0x01001F67` is the smaller secondary moon.
Retail does **not** provide a separate lighting colour or intensity for each
mesh. `SkyDesc::GetLighting` produces one interpolated directional vector,
colour, and brightness from `SkyTimeOfDay.DirHeading`, `DirPitch`, `DirColor`,
and `DirBright`. The opt-in atmospheric pack therefore uses the selected
visible celestial mesh only for shadow **direction**. Colour and energy remain
the single AC-authored directional-light values.
The deterministic priority is:
1. visible sun whose transformed centre is above the horizon;
2. visible large/haloed moon whose transformed centre is above the horizon;
3. visible secondary moon whose transformed centre is above the horizon;
4. no directional shadow source.
This is a pack enhancement, not a claim that retail cast real-time moon
shadows.
## Evidence and provenance
The investigation followed the project rendering inventory and used the
already-loaded retail structures rather than inventing another sky model.
Evidence came from:
- `artifacts/atmospheric-rendering/sky-heading-dump/client.log`, especially
lines 40-88 for Sunny day group 0 and the corresponding repeated entries for
all later day groups. The dump records the three IDs, visibility windows,
angular sweeps, keyframe directional lighting, and the sun surface.
- A read-only `DatCollection.Get<GfxObj>`/`Get<Surface>` probe against the
installed Asheron's Call DATs, using the same inspection path implemented by
`tools/SkyObjectInspect/Program.cs`, for all three `GfxObj` sort centres,
polygon geometry, surfaces, and texture chains.
- `tools/RainMeshProbe/Program.cs` lines 37-49, which names and audits the
celestial surface set independently of the shadow implementation.
- `docs/research/named-retail/acclient_2013_pseudo_c.txt`:
`SkyDesc::GetLighting` at `0x00500a80` (around line 261291),
`SkyDesc::GetSky` at `0x00501ec0` (around line 262761),
`GameSky::CalcFrame` at `0x00506f80` (around line 268650), and
`GameSky::UseTime` at `0x005075b0` (around line 269090).
- `docs/research/2026-04-23-sky-retail-verbatim.md`, especially its recorded
directional-light interpolation and `GameSky::UseTime` material updates.
No fresh decompilation was required. The named-retail corpus already answered
the only question the current code and DAT dump could not answer on their own:
whether a moon mesh contributes a second retail world light. It does not.
## Installed-DAT characterization
The following values were read from the installed Dereth Region and the three
referenced `GfxObj`/surface/texture chains. The same three object IDs, windows,
and sweeps occur in every one of the 20 day groups; only their object index
changes between seven-object and weather-heavy groups.
| Role | GfxObj | Day window | Angular sweep | Authored `SortCenter` |
|---|---:|---:|---:|---:|
| Sun disk | `0x01001348` | `0.1600..0.9400` | `-23 deg..203 deg` | `(1050, 0, 0)` |
| Secondary moon | `0x01001F67` | `0.0400..0.2100` | `-20 deg..190 deg` | `(1909.46, 1874.78, -0.0000157485)` |
| Dominant moon + halo | `0x01001F6A` | `0.0000..0.2300` | `-20 deg..190 deg` | `(2066.82, 552.99, 0)` |
The asset chain establishes the visual identities and the dominant-moon
choice:
| GfxObj | Surface | Surface flags | SurfaceTexture | RenderSurface | Image |
|---:|---:|---|---:|---:|---|
| `0x01001348` | `0x080000D1` | Base1Image, Alpha, Additive | `0x050014CD` | `0x0600388D` | 128x128 `PFID_R8G8B8` sun disk |
| `0x01001F67` | `0x080000D2` | Base1ClipMap | `0x05001A6C` | `0x06003894` | 256x256 `PFID_INDEX16`, palette `0x0400103F` |
| `0x01001F6A` | `0x080000D6` | Base1ClipMap | `0x05001A6D` | `0x06003898` | 256x256 `PFID_INDEX16`, palette `0x0400103F` |
| `0x01001F6A` | `0x080000D7` | Base1Image, Alpha, Additive | `0x05001A6E` | `0x06003899` | 128x128 `PFID_R8G8B8` halo |
Every listed surface has authored `Luminosity=1`, `Diffuse=1`, and
`Translucency=0`. The large moon's primary quad has roughly 2.3 times the
polygon area of the secondary moon before its still larger additive halo is
counted. That makes `0x01001F6A` the unambiguous dominant lunar visual when
both moons are above the horizon.
These installed-DAT facts are characterization evidence, not an ordinary test
dependency. Unit tests use hand-built `DayGroupData` so clean CI and machines
without retail DATs remain deterministic.
## Direction and visibility contract
`SkyObjectData.IsVisible(dayFraction)` owns the normal, always-visible, and
midnight-wrapping window cases. `CurrentAngle(dayFraction)` owns the authored
arc interpolation, including progress through a wrapping window.
The selected direction must match the sky renderer exactly:
```text
heading = active SkyObjectReplace.Rotate
arc = SkyObjectData.CurrentAngle(dayFraction)
model = RotationZ(-heading) * RotationY(-arc)
anchor = effective GfxObj.SortCenter
direction = normalize(TransformNormal(anchor, model))
```
“Effective” means that an active non-zero replacement `GfxObjId` also supplies
its own `SortCenter`. A replacement with `Transparent >= 1` makes the object
ineligible. The replacement lookup follows the renderer's discrete active
keyframe rule; it does not interpolate replacement fields. A zero, non-finite,
or below/on-horizon transformed direction is ineligible.
This deliberately does not substitute `SkyTimeOfDay.DirHeading/DirPitch` for
moon direction. Those values are the one retail world-light direction. The
moon meshes have separate authored arcs, and the enhancement is specifically
intended to align moon shadows with the moon the player can see.
## Authored light contribution
Named retail `SkyDesc::GetLighting` interpolates the two surrounding
`SkyTimeOfDay` records and produces:
```text
sunVector = DirBright * (
cos(DirPitch) * sin(DirHeading),
cos(DirPitch) * cos(DirHeading),
sin(DirPitch))
directionalColor = DirColor * length(sunVector)
```
`length(sunVector)` is `DirBright`. acdream exposes the resulting colour as
`SkyKeyframe.SunColor`. The pack's scalar authored energy is therefore
`clamp(max(SunColor.r, SunColor.g, SunColor.b), 0, 1)`.
By contrast, named retail `GameSky::UseTime` sends a celestial replacement's
`Luminosity`, `MaxBright`, and `Transparent` to the mesh material through
`SetLuminosity`, `SetDiffusion`, and `SetTranslucency`. It does not install a
second directional light. Texture brightness and moon surface luminosity must
not manufacture extra world-light energy.
Weather/day-group reductions, softness, and elevation ramps remain explicit
render-pack policy. They are not mislabelled as measured retail intensities.
## Parity and safety registration
### Retail behavior
- One interpolated directional world-light channel comes from
`SkyTimeOfDay.Dir*`.
- Celestial meshes follow their own visibility windows and transformed arcs.
- Replacement luminosity/diffusion/transparency changes mesh material state,
not the number of world-directional lights.
- Retail does not render the Campaign AR cascaded real-time object shadows.
### Opt-in pack enhancement
- The pack chooses the visible sun or dominant visible moon direction for its
directional shadow map.
- Moon direction follows the rendered moon; energy remains the single
AC-authored directional channel.
- Sun wins any overlap when its transformed centre is above the horizon;
otherwise the haloed moon wins before the secondary moon.
- This deviation belongs in the atmospheric render-pack entry of
`docs/architecture/retail-divergence-register.md`.
### Unchanged boundaries
- The retail rendering path remains the default and authoritative output.
- Pack-off frames do not resolve or render celestial shadow work.
- Existing retail scene lighting remains driven by `SkyStateProvider`; this
policy does not replace it.
- Physics, collision, containment, selection, movement, and DAT geometry are
untouched. The selected source is an immutable one-frame rendering fact.
## Deterministic acceptance coverage
`tests/AcDream.App.Tests/Rendering/Packs/AuthoredCelestialShadowSourceResolverTests.cs`
locks:
- the three verified IDs and priority independent of object-list order;
- sun overlap, dominant-moon fallback, and secondary-moon fallback;
- fully transparent and effective replacement behavior;
- replacement rotation and the exact renderer transform direction;
- no-visible/no-above-horizon suppression;
- midnight-wrapping visibility and angle progress; and
- directional colour-times-brightness energy, including preservation when no
celestial source is available.
The test fixture is entirely hand-built. It neither requires nor silently
substitutes installed retail DAT content.
The complete non-physical verification result, including shader ABI, exact
retail-binary preservation, performance/lifetime fixtures, locked restore,
Release build, and fresh-process totals, is recorded in the
[Campaign AR Stage 1 automated gate report](2026-08-22-atmospheric-stage1-automated-gate.md).