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>
432 lines
19 KiB
Markdown
432 lines
19 KiB
Markdown
# Sky Decompile Hunt B — D3D Render-State Signature Trace
|
||
|
||
**Date:** 2026-04-23
|
||
**Hunter:** Hunt Agent B (render-state signatures)
|
||
**Status:** SIGNIFICANT FINDINGS — but NOT a "celestial-body iteration draw loop"
|
||
|
||
## TL;DR
|
||
|
||
The retail acclient does NOT appear to have a classical "sky dome + iterate
|
||
celestial meshes with per-mesh blend swaps" render function. Instead, sky
|
||
is implemented as:
|
||
|
||
1. **A per-frame keyframe sampler** (`FUN_005062e0`) that interpolates the
|
||
current RegionDesc keyframe (sun angle, sun color, ambient color, fog
|
||
color/near/far) and stashes results into globals.
|
||
2. **Per-mesh ambient color** pushed via `_DAT_008682bc/c0/c4` (RGB floats,
|
||
no D3DRS_AMBIENT call).
|
||
3. **D3D fog state** (D3DRS_FOGCOLOR/FOGSTART/FOGEND) set via
|
||
`FUN_005a41b0` — this is the ONLY sky-keyframe → D3D state write path.
|
||
4. **A "pre-world" pass** (`FUN_00507a50(0)`) that disables Zwrite and
|
||
iterates weather volume objects (fog/rain/snow, NOT sun/moon/stars).
|
||
|
||
No "celestial body draw loop" found. No D3DRS_AMBIENT call found. No
|
||
matrix-translation-zero (camera-anchor) found. No lightning RNG found.
|
||
No huge far-plane constant found at the sky call site. This strongly
|
||
suggests retail AC is far simpler than modern sky demos assume — the
|
||
sun/moon/stars are either baked into the RegionDesc scene geometry
|
||
(treated as regular objects that happen to be positioned far away) or
|
||
do not exist as discrete draw calls at all, with "sun color" being
|
||
expressed purely via per-vertex lighting on world meshes.
|
||
|
||
## D3D Vtable Map
|
||
|
||
All state writes go through `pIDirect3DDevice` at `(param_1 + 0x468)`,
|
||
vtable offset `0xe4` = `SetRenderState`, offset `0xd4` = `SetTextureStageState`,
|
||
offset `0xc4` = `SetMaterial`, offset `0x1c` (of a PARENT wrapper object at
|
||
`DAT_0086734c`) = `SetPerspective/SetFrustum`.
|
||
|
||
Discovered D3D state wrappers in `chunk_005A0000.c`:
|
||
|
||
| Wrapper | Addr | D3DRS | Meaning | Cache offset |
|
||
|---|---|---|---|---|
|
||
| FUN_005a3ba0 | 0x005A3BA0 | 0x1b=27 | ALPHABLENDENABLE | +0x475 |
|
||
| FUN_005a3be0 | 0x005A3BE0 | 0x0f=15 | ALPHATESTENABLE | +0x476 |
|
||
| FUN_005a3c20 | 0x005A3C20 | 0x19=25 | ALPHAFUNC | +0x478 |
|
||
| FUN_005a3c60 | 0x005A3C60 | 0x18=24 | ALPHAREF | +0x47c |
|
||
| FUN_005a3ca0 | 0x005A3CA0 | 0x13=19 src, 0x14=20 dst, 0xab=171 op | blend state | +0x4f0/+0x49c |
|
||
| FUN_005a3d80 | 0x005A3D80 | 0x16=22 | CULLMODE | +0x488 |
|
||
| FUN_005a3dc0 | 0x005A3DC0 | 0xaf=175 | (fog-gated) | +0x48c |
|
||
| FUN_005a3e00 | 0x005A3E00 | 7 | ZENABLE | none (stateless) |
|
||
| FUN_005a3e20 | 0x005A3E20 | 0x17=23 (ZFUNC) + 0xe=14 (ZWRITE) | combined ZFunc+ZWrite | +0x494/+0x498 |
|
||
| FUN_005a3eb0 | 0x005A3EB0 | 0x8b=139 | **AMBIENT** | +0x4a0 |
|
||
| FUN_005a3ef0 | 0x005A3EF0 | 0x91=145 | COLORVERTEX | +0x4a8 |
|
||
| FUN_005a3f40 | 0x005A3F40 | 0x93=147 | ? | +0x4a4 |
|
||
| FUN_005a3f90 | 0x005A3F90 | 0x1c=28 | **FOGENABLE** | +0x4bc |
|
||
| FUN_005a4080 | 0x005A4080 | 0x22=34 (FOGCOLOR), 0x24=36 (FOGSTART), 0x25=37 (FOGEND) | fog triple | +0x4ac/+0x4c0/+0x4c4 |
|
||
| FUN_005a41b0 | 0x005A41B0 | wrapper → FUN_005a4080 | **SetFog(color, start, end)** | — |
|
||
| FUN_005a41f0 | 0x005A41F0 | 0x89=137 | **LIGHTING** | +0x4c8 |
|
||
| FUN_005a4280 | 0x005A4280 | 0x92=146 | LOCALVIEWER | +0x4d8 |
|
||
| FUN_005a4350 | 0x005A4350 | 0x3c=60 | ? | +0x4e0 |
|
||
| FUN_005a4390 | 0x005A4390 | 0x08 | SHADEMODE | +0x4e4 |
|
||
| FUN_005a4420 | 0x005A4420 | stages | SetTextureStageState blend args | — |
|
||
| FUN_005a4010 | 0x005A4010 | internal flag at +0x4be → re-calls FUN_005a3f90 | **fog-master-enable gate** | +0x4be |
|
||
|
||
**Device-reset-to-default** function: `FUN_005a10f0` at 0x005A10F0, lines
|
||
687-740 of chunk_005A0000.c. This is NOT the sky render; it is called
|
||
once on device init and full-state rebuild.
|
||
|
||
## AMBIENT (D3DRS=0x8b=139) — The Key Negative Finding
|
||
|
||
I searched every call to vtable `0xe4` with state `0x8b` across the
|
||
entire 688K-line decompile. Results:
|
||
|
||
- chunk_005A0000.c:3388 — `...0x8b,0` — inside default-init reset
|
||
- chunk_005A0000.c:2772 — `...0x8b,param_2` — inside wrapper FUN_005a3eb0
|
||
|
||
External callers of `FUN_005a3eb0`:
|
||
|
||
```
|
||
chunk_005A0000.c:704: FUN_005a3eb0(0); // default-reset, ambient=black
|
||
```
|
||
|
||
**ONLY ONE CALLER.** The sky keyframe sampler (`FUN_005062e0`) does NOT
|
||
call `FUN_005a3eb0`. D3DRS_AMBIENT is set to 0 once and never changed.
|
||
**This falsifies the previous research hypothesis** (2026-04-22 doc) that
|
||
clouds are tinted by a per-keyframe D3DRS_AMBIENT write.
|
||
|
||
Sky color instead lives in `_DAT_008682bc/c0/c4` (three floats, RGB, set
|
||
from `DAT_00842950/54/58`). See below.
|
||
|
||
## Sky Keyframe Sampler — `FUN_005062e0` @ 0x005062E0
|
||
|
||
chunk_00500000.c, lines 6213-6333. Called each frame from the scene
|
||
renderer (`FUN_00455a50` path, around chunk_00450000.c:4341, 4468).
|
||
|
||
Stripped pseudocode:
|
||
|
||
```c
|
||
void FUN_005062e0(int worldRenderer) {
|
||
if (worldRenderer.layerBits != 0 && worldRenderer.skyEnabled != 0)
|
||
{
|
||
FUN_00508010(); // advance sky timer
|
||
double now = _DAT_008379a8; // game-time seconds
|
||
if (_DAT_00842798 <= now && currentRegionDesc != 0) {
|
||
_DAT_00842798 = regionDesc[+0x50].nextKeyTime + now; // reschedule
|
||
|
||
float turbidity = (weatherSys ? weatherSys[+0x48] : _DAT_00795610);
|
||
if (_DAT_008427a0 < now) {
|
||
// ---- BLOCK 1: sample ambient/sun ----
|
||
int ok = FUN_004ff440(turbidity,
|
||
&fogDensity, // out float
|
||
&sunColor, // out ARGB
|
||
sunDirVec, // out float[3]
|
||
&ambientColor); // out ARGB
|
||
if (ok) {
|
||
if (fogDensity < DAT_0084295c) fogDensity = DAT_0084295c;
|
||
// ---- WEATHER FOG BLEND (if weather active) ----
|
||
if (DAT_008427a9 /* weatherFogEnabled */) {
|
||
if (1.0f <= _DAT_008427b8 /* blendProgress */) {
|
||
// blend complete; snap to weather values
|
||
fogDensity = DAT_008427ac; // weather density
|
||
sunColor = DAT_00842788; // weather fog color
|
||
} else {
|
||
// lerp currentColor → weatherColor per channel (bytes),
|
||
// lerp density toward DAT_008427ac
|
||
// advance _DAT_008427b8 by _DAT_007c7208 (blend rate)
|
||
}
|
||
}
|
||
FUN_00505f30(fogDensity, sunColor, sunDirVec, ambientColor);
|
||
}
|
||
_DAT_008427a0 = now + (currentRegionDesc ?
|
||
regionDesc[+0x50].keyInterval : _DAT_007c7200);
|
||
}
|
||
|
||
// ---- BLOCK 2: sample fog near/far/color ----
|
||
FUN_005a4010(!DAT_0081dbf8); // disable fog if master fog flag clear
|
||
if (DAT_0081dbf8) {
|
||
FUN_005a3f90(DAT_0081dbf8); // FOGENABLE = true
|
||
int ok2 = FUN_004ff480(turbidity,
|
||
&fogNear, // out float
|
||
&fogFar, // out float
|
||
&fogColor);// out ARGB
|
||
if (ok2) {
|
||
if (DAT_008427a9 /* weatherFogEnabled */) {
|
||
// blend fogNear, fogFar, fogColor toward weather values
|
||
// identical lerp structure
|
||
}
|
||
FUN_005a41b0(&fogColor, fogNear, fogFar);
|
||
// → SetRenderState(FOGCOLOR, ...), (FOGSTART, ...), (FOGEND, ...)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
This is the ONLY per-frame writer of fog D3D state. Called by
|
||
`FUN_00455a50` (scene entry) via `FUN_00506270` etc.
|
||
|
||
## Block-1 Keyframe Layout — `FUN_00501600` @ 0x00501600
|
||
|
||
chunk_00500000.c, lines 1151-1232. Decoded keyframe struct:
|
||
|
||
| Offset | Field | Type | Notes |
|
||
|---|---|---|---|
|
||
| +0x04 | SunMagnitude | float | lerp → param_4 (radius) |
|
||
| +0x08 | SunYAngle | float | degrees; `* (π/180)` gives radians |
|
||
| +0x0c | SunZAngle | float | degrees; `* (π/180)` gives radians |
|
||
| +0x10–0x12 | AmbientColor | byte[3] | R,G,B (packed via byte-shuffles) |
|
||
| +0x14 | FogDensity | float | lerp → param_2 |
|
||
| +0x18–0x1a | SunColor | byte[3] | R,G,B |
|
||
|
||
From chunk_00500000.c:1190-1216:
|
||
|
||
```c
|
||
// out sunDirVec (param_4):
|
||
fVar9 = lerp(nextKey[+0x04], prevKey[+0x04], param_1); // magnitude
|
||
float sinAngY = lerp(nextKey[+0x08], prevKey[+0x08]) * DEG2RAD;
|
||
float sinAngZ = lerp(nextKey[+0x0c], prevKey[+0x0c]) * DEG2RAD;
|
||
float cosY = cos(sinAngY);
|
||
float sinY = sin(sinAngY);
|
||
float cosZ = cos(sinAngZ);
|
||
float sinZ = sin(sinAngZ);
|
||
param_4[0] = fVar9 * sinZ * cosY;
|
||
param_4[1] = fVar9 * cosZ * cosY;
|
||
param_4[2] = fVar9 * sinY;
|
||
|
||
// out sunColor ARGB (param_3): from bytes at +0x18,0x19,0x1a
|
||
// out ambientColor ARGB (param_5): from bytes at +0x10,0x11,0x12
|
||
```
|
||
|
||
(The repeated `FUN_005df4c4()` calls with no args are Ghidra's artifact
|
||
for a union/reinterpret-cast byte shuffle — they are NOT an RNG and NOT
|
||
a lightning modulator.)
|
||
|
||
## Block-2 Keyframe Layout — `FUN_00501860` @ 0x00501860
|
||
|
||
chunk_00500000.c, lines 1236-1268. Second block of the keyframe struct:
|
||
|
||
| Offset | Field | Type | Notes |
|
||
|---|---|---|---|
|
||
| +0x20 | FogNear | float | |
|
||
| +0x24 | FogFar | float | |
|
||
| +0x28–0x2a | FogColor | byte[3] | R,G,B |
|
||
|
||
## Apply-Block-1 — `FUN_00505f30` @ 0x00505F30
|
||
|
||
chunk_00500000.c, lines 6026-6092. Stores the sampled values into global
|
||
cache and into the `_DAT_008682bc/c0/c4` "per-vertex ambient" slot:
|
||
|
||
```c
|
||
DAT_00842780 = fogDensity; // param_2
|
||
DAT_0084277c = sunColor; // param_3 (ARGB)
|
||
DAT_00842950 = sunDir[0]; // param_4[0]
|
||
DAT_00842954 = sunDir[1];
|
||
DAT_00842958 = sunDir[2];
|
||
DAT_00842778 = ambientColor; // param_5 (ARGB)
|
||
|
||
_DAT_008682c8 = sunDir[0]; // duplicated
|
||
_DAT_008682d0 = sunDir[2];
|
||
_DAT_008682cc = sunDir[1];
|
||
FUN_00451a60(&_something, ambientColor); // unpacks ARGB → 3 floats
|
||
_DAT_008682bc = unpacked R; // RGB ambient floats for mesh lighting
|
||
_DAT_008682c0 = unpacked G;
|
||
_DAT_008682c4 = unpacked B;
|
||
DAT_008682d4 = 0; // ambient dirty flag
|
||
```
|
||
|
||
**`_DAT_008682bc/c0/c4` is the THING the sky tints the world with.**
|
||
It is consumed per-vertex by mesh-draw, not by a D3D state write.
|
||
|
||
## Candidate "Sky Pass" — `FUN_00507a50` @ 0x00507A50
|
||
|
||
chunk_00500000.c, lines 7250-7299.
|
||
|
||
```c
|
||
void FUN_00507a50(weatherMgr, phase) {
|
||
if ((FUN_00451ec0() || phase == 0)) { // skyEnabled or pass-0
|
||
weatherMgr.renderingFlag = 1;
|
||
char oldAlpha = FUN_005a1560();
|
||
FUN_005a3f90(DAT_008427a9 != '\0'); // FOGENABLE ← weatherFog flag
|
||
float oldFar = DAT_0081fc98;
|
||
FUN_0054bf30(DAT_0081fc98 * _DAT_007c6f14); // multiply far plane
|
||
FUN_005a3e20(8, 0); // ZFUNC=ALWAYS, ZWRITE=0
|
||
if (phase == 0) {
|
||
// iterate weather volume objects (rain/snow/fog cells)
|
||
for (uint i = 0; i < weatherMgr.count; ++i) {
|
||
int obj = *(int*)(weatherMgr.pArray + i*4);
|
||
uint flags = *(uint*)(weatherMgr.pFlags + i*4);
|
||
if (obj && !(flags & 1) &&
|
||
(DAT_0081dbf9 || !(flags & 4)) &&
|
||
(!DAT_008427a9 || !FUN_005a1560() || !(flags_byte & 2)))
|
||
{
|
||
FUN_00511720(obj); // scene-graph walk (update per-frame)
|
||
FUN_00511760(obj); // scene-graph walk (draw)
|
||
}
|
||
}
|
||
} else if (DAT_0081dbf9) {
|
||
// phase 1: just let the device callback at +0x64 run
|
||
(**(code **)(*DAT_00870340 + 0x64))(weatherMgr[+0x28]);
|
||
}
|
||
FUN_0054bf30(oldFar); // restore far plane
|
||
FUN_005a3e20(4, 1); // ZFUNC=LESSEQUAL, ZWRITE=1
|
||
FUN_005a3f90(oldAlpha); // restore FOGENABLE
|
||
weatherMgr.renderingFlag = 0;
|
||
}
|
||
}
|
||
```
|
||
|
||
Called by `FUN_00506d90` (chunk_00500000.c:6683):
|
||
|
||
- `FUN_00507a50(0)` = phase-0 (sky-ish volume draw, before terrain/entity)
|
||
- `FUN_00507a50(1)` = phase-1 (post-draw overlay, gated by weather flag)
|
||
|
||
The `DAT_0081fc98 * _DAT_007c6f14` line IS the far-plane multiplier —
|
||
however `_DAT_007c6f14` here is ambiguous, used elsewhere as a small
|
||
rotation coefficient (chunk_005E0000.c:258 etc). This deserves a
|
||
follow-up: check if that DAT is a configurable sky-far-plane multiplier
|
||
or a recycled constant.
|
||
|
||
**No matrix-translation-zero (camera-anchor) is present in this
|
||
function.** The scene graph objects iterated via `FUN_00511720/60` are
|
||
rain/snow/fog-shaft volumes — they are positioned in world space and
|
||
expected to follow the camera via normal scene-graph propagation.
|
||
|
||
**No per-mesh blend swap (alpha for clouds vs additive for sun) is
|
||
present.** Blend mode is set once for the whole pass.
|
||
|
||
## Fog Write Path — SUMMARY
|
||
|
||
The complete trail:
|
||
|
||
1. `FUN_00505f30` stores sun/ambient globals (no D3D call).
|
||
2. `FUN_005062e0` lerps current-keyframe fog → weather-fog if needed.
|
||
3. `FUN_005a41b0(&fogColor, fogNear, fogFar)` →
|
||
4. `FUN_005a4080(pFogColor_as_ARGB, near, far)` →
|
||
5. Writes D3DRS_FOGCOLOR=0x22, D3DRS_FOGSTART=0x24, D3DRS_FOGEND=0x25.
|
||
|
||
Fog ENABLE is wrapped by `FUN_005a4010` (master gate at +0x4be) +
|
||
`FUN_005a3f90` (actual D3DRS_FOGENABLE=0x1c write). Both driven by
|
||
the DAT_0081dbf8 master-fog flag.
|
||
|
||
**D3DRS_FOGTABLEMODE=0x23, FOGVERTEXMODE=0x8c, FOGDENSITY=0x26** —
|
||
these are only set once in the default-init (`FUN_005a10f0`) and never
|
||
per-frame. Retail uses linear fog (FOGSTART/FOGEND), not exponential
|
||
(FOGDENSITY).
|
||
|
||
## Lightning Flash — NOT FOUND
|
||
|
||
Searched for:
|
||
|
||
- `FUN_005df4c4` (the RNG/byte-shuffle) — all hits are byte-packing
|
||
for ARGB color interpolation, not random modulation.
|
||
- `_DAT_008427b8` — it's a weather-fog-blend progress (0 → 1 ramp),
|
||
not a per-frame random value.
|
||
- Time-based sine/cos on small periods — none found coupled to sky
|
||
color.
|
||
- Any RNG call near fog/ambient writes — none found.
|
||
|
||
**Lightning is likely a separate system not found in this hunt, OR
|
||
does not exist in retail and is purely decorative in modern ports.**
|
||
|
||
## Matrix-Anchor (Camera-Centered Sky) — NOT FOUND
|
||
|
||
Searched for patterns like:
|
||
|
||
- `*(undefined4*)(mat + 0x30) = 0;`
|
||
- `SetTransform(D3DTS_VIEW=2, ...)` via any vtable offset
|
||
- A matrix copy that zeroes three consecutive 4-byte fields
|
||
|
||
Nothing found anchored to the sky pass. The view matrix is NOT rewritten
|
||
with zero translation before the sky draw. This is consistent with the
|
||
conclusion that there is no discrete "sky dome" — the weather/fog volume
|
||
objects follow the camera by being placed in camera-relative world
|
||
position by their parent scene-graph node.
|
||
|
||
## Huge Far-Plane Constants — NOT FOUND
|
||
|
||
Searched for:
|
||
|
||
- 0x49742400 (1e6f) — no hits
|
||
- 0x47c35000 (1e5f) — no hits
|
||
- 0x4b189680 (1e7f) — no hits
|
||
|
||
The only far-plane modulator is `DAT_0081fc98 * _DAT_007c6f14` at
|
||
chunk_00500000.c:7272. Without knowing `_DAT_007c6f14`'s numeric value
|
||
from a debugger or disassembly of `.rdata`, we can't tell if this is
|
||
>>1 (sky-far-plane multiplier) or ~1 (no-op). A follow-up
|
||
disassembly of the address `0x007c6f14` should nail this down.
|
||
|
||
## D3D State Inventory — Sky-Relevant Per-Frame Writes
|
||
|
||
Only four D3D states are written per-frame in the sky-adjacent code path:
|
||
|
||
| State | Hex | Name | Writer | Source of value |
|
||
|---|---|---|---|---|
|
||
| 34 | 0x22 | FOGCOLOR | `FUN_005a4080` | current keyframe+weather lerp |
|
||
| 36 | 0x24 | FOGSTART | `FUN_005a4080` | current keyframe+weather lerp |
|
||
| 37 | 0x25 | FOGEND | `FUN_005a4080` | current keyframe+weather lerp |
|
||
| 28 | 0x1c | FOGENABLE | `FUN_005a3f90` | `DAT_0081dbf8` / weather flag |
|
||
|
||
Plus, inside `FUN_00507a50` (phase-0 weather-volume draw only):
|
||
|
||
| State | Hex | Name | Value |
|
||
|---|---|---|---|
|
||
| 23+14 | 0x17+0x0e | ZFUNC + ZWRITE | ALWAYS + OFF → restore LESSEQUAL + ON |
|
||
|
||
## Gaps / Unresolved
|
||
|
||
1. **The classical sky dome / celestial body draw** — NOT FOUND. Either
|
||
retail literally does not have one (sky is just a clear-color glimpse
|
||
plus fog between camera and terrain), or it's hidden inside the
|
||
RegionDesc.Scene entries and rendered as a regular object through the
|
||
normal scene-graph path.
|
||
2. **`_DAT_007c6f14`** — used as a far-plane multiplier in the sky pass,
|
||
but cross-referenced use in chunks 0x5E0000 suggests it's a unit-scale
|
||
float (< 10). Needs raw `.rdata` dump to confirm its value.
|
||
3. **Is `_DAT_008682bc/c0/c4` consumed by D3D material or per-vertex
|
||
color?** The writers are clear; the readers need a separate hunt
|
||
(likely in chunk_00590000.c or the mesh-draw path at `FUN_0054c9c0`).
|
||
4. **Lightning flash** — not found in the keyframe/fog paths. If it
|
||
exists, it's in a separate code path (maybe tied to weather presets
|
||
6 "thunderstorm" — DAT_008427a9=1 case in chunk_00550000.c:11886).
|
||
|
||
## Files / Addresses cited (all absolute paths)
|
||
|
||
- `C:\Users\erikn\source\repos\acdream\docs\research\decompiled\chunk_005A0000.c`
|
||
- lines 687-740: `FUN_005a10f0` device reset (AMBIENT=0 here, only caller)
|
||
- lines 2606-3035: D3D state wrapper zoo (all vtable-0xe4 writers)
|
||
- lines 2868-2907: `FUN_005a4080` = SetFog triple
|
||
- lines 2911-2921: `FUN_005a41b0` = SetFog wrapper
|
||
- lines 3346-3400: default state-init defaults
|
||
- lines 3860-3970: `FUN_005a5950` world-scene top-level renderer
|
||
- `C:\Users\erikn\source\repos\acdream\docs\research\decompiled\chunk_00500000.c`
|
||
- lines 1151-1232: `FUN_00501600` Block-1 keyframe sampler
|
||
- lines 1236-1268: `FUN_00501860` Block-2 (fog) keyframe sampler
|
||
- lines 6026-6092: `FUN_00505f30` apply-keyframe-1 to globals
|
||
- lines 6213-6333: `FUN_005062e0` per-frame sky+fog update (MAIN)
|
||
- lines 6683-6708: `FUN_00506d90` scene renderer (calls sky phase 0/1)
|
||
- lines 7250-7299: `FUN_00507a50` the phase-0 weather-volume draw
|
||
- `C:\Users\erikn\source\repos\acdream\docs\research\decompiled\chunk_00450000.c`
|
||
- lines 608-622: `FUN_00451a60` ARGB → 3-float unpack
|
||
- lines 4300-4479: per-landblock ambient setup (`_DAT_008682bc` writers)
|
||
- `C:\Users\erikn\source\repos\acdream\docs\research\decompiled\chunk_00550000.c`
|
||
- lines 11835-12016: `FUN_0055eb40` weather-preset-id → fog constants
|
||
(0=clear, 1=light rain, 2=medium, 3=heavy, 4=blizzard, 5=?, 6=thunderstorm)
|
||
- `C:\Users\erikn\source\repos\acdream\docs\research\decompiled\chunk_004F0000.c`
|
||
- lines 10692-10720: `FUN_004ff440` / `FUN_004ff480` keyframe trampolines
|
||
|
||
## Recommendation for acdream port
|
||
|
||
Given these findings, our C# SkyRenderer should:
|
||
|
||
1. **Stop expecting a celestial-body draw loop in the decompile** — Hunt A
|
||
will likely confirm via the Region-loader angle.
|
||
2. **Port the keyframe sampler faithfully** from `FUN_00501600` / 1860 —
|
||
the struct layout above is the ground truth for RegionDesc keyframe
|
||
blobs.
|
||
3. **Apply the sun direction and colors to world-mesh vertex lighting**,
|
||
not to a D3DRS_AMBIENT equivalent. The values live in
|
||
`_DAT_008682bc/c0/c4` and are consumed by the mesh pipeline.
|
||
4. **Set fog via SetFog(color, start, end) + FogEnable** per frame from
|
||
the Block-2 sample. This matches `FUN_005a41b0`.
|
||
5. **Do NOT add a matrix translation-zero** — retail does not anchor the
|
||
sky dome, because retail does not appear to HAVE a sky dome draw in
|
||
the D3D state sense. If acdream renders a dome mesh, it's a modern
|
||
addition.
|
||
6. **Tint clouds (if rendered) via the per-mesh ambient global**, not
|
||
via the current uniform-one approach. The previous research doc's
|
||
conclusion on "clouds multiply by ambient" is likely correct in
|
||
spirit — just know that "ambient" here means
|
||
`_DAT_008682bc/c0/c4`, not D3DRS_AMBIENT.
|