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.
31 KiB
Sky Decompile Hunt C — Globals & Keyframe Interpolator
Date: 2026-04-23
Hunter: Agent C (globals / keyframe math)
Scope: Find the runtime state block the sky renderer reads, the keyframe-interp math, and the per-frame entry point.
Source tree: C:\Users\erikn\source\repos\acdream\docs\research\decompiled\chunk_*.c (55 chunks, 688K lines).
All citations use {chunk_file}:{line} relative to the decompile tree.
⚠ 2026-04-24 correction
Sections §1, §2, §5 of this doc label DAT_00842778 as "AmbColor" and
DAT_0084277c as "DirColor/Fog". That labeling is backwards. The
correct mapping — cross-verified against the DatReaderWriter schema
(SkyTimeOfDay.Unpack field order) and the FUN_00501600 output map:
DAT_00842778= DirColor (directional/sun color ARGB)DAT_0084277c= AmbColor (ambient color ARGB)DAT_00842780= AmbBright (ambient brightness scalar, not fog start)
The FUN_00532440 per-vertex Lambert at chunk_00530000.c:2118-2124
reads DAT_00842778 as the N·L-modulated color (→ directional) and
DAT_0084277c × DAT_00842780 as the flat / brightness-scaled color
(→ ambient × ambBright). The pre-multiply at line 2107 takes
DAT_00842780 * DAT_0084277c which is the textbook "ambient scalar ×
ambient color" retail ambient term.
See docs/research/2026-04-24-lambert-brightness-split.md for the full
re-analysis and SkyTimeOfDay.generated.cs for the field offsets (+0x10
DirColor, +0x18 AmbColor). All entries below should be read with this
swap in mind; the decompile math quotes themselves are correct.
1. Global Inventory — the sky state block
All globals live in a contiguous block at 0x00842778..0x008427c0 with a second cluster at 0x00842950..0x00842960. Every field is read by landblock/draw code and written only by the per-frame updater FUN_005062e0 via the interp delegate FUN_00501600. Initial values are set in FUN_00505dd0 (the sky-system constructor).
| Address | Size | Inferred semantic | Initial value | Writers | Readers |
|---|---|---|---|---|---|
DAT_00842778 |
4 B (ARGB packed u32) | AmbColor / SunLight color — unpacked to 3 floats via FUN_00451a60 into the active D3D material/light |
0 (default white via fallback) |
FUN_00505f30:6047; FUN_00501600 (via out-param param_5) |
FUN_00451a60(DAT_00842778) in chunk_00450000.c:4341, 4468; chunk_00500000.c:6061 — applied to D3D light state |
DAT_0084277c |
4 B (ARGB packed u32) | Fog color — passed to FUN_004530e0(fogStart, DAT_0084277c) and also decomposed into 3 floats (R,G,B)*1/255 for per-vertex terrain lighting |
0 |
FUN_00505f30:6042 (param_3); FUN_00501600 (via param_3) |
chunk_00530000.c:2095,2097,2099 (terrain lighting); chunk_00500000.c:6069; chunk_00450000.c:4347,4474 |
DAT_00842780 |
4 B (float) | Fog start distance / luminosity offset — added to |sun|·_DAT_0079a1e8 and passed as first arg to fog setup FUN_004530e0 |
0x3ecccccd = 0.4f |
FUN_00505f30:6041 (param_2); FUN_00501600 (via out-param param_2) |
chunk_00500000.c:6067-6069; chunk_00530000.c:2094, 2107-2109 (terrain mul) |
DAT_00842784 |
4 B (ARGB u32) | Sky-fog secondary color (bucket pick helper — see §6) | 0 |
chunk_00550000.c:11851/61/67/80/92 (palette decision in render-sample path), FUN_005062e0:6295,6301 |
chunk_00500000.c:6295,6301,6305,6311,6317 |
DAT_00842788 |
4 B (ARGB u32) | Sky-fog primary color (same role, paired with 0x842784) | 0 |
chunk_00550000.c:11850/62/67/80/91, FUN_005062e0:6251,6259 |
chunk_00500000.c:6251,6259,6263,6269,6275 |
DAT_00842790 |
4 B (ptr) | Cloud / stars heightmap buffer — operator_delete__'d on teardown, re-alloc'd to (N+1)*0x100 bytes where N is cloud-layer count |
NULL |
chunk_00500000.c:5994, 6543 |
chunk_00500000.c:6571, 6586, 6597, 6599, 6539 |
_DAT_00842798 |
8 B (double) | Next cloud/weather update deadline — compared to the global game-clock _DAT_008379a8 each frame, advanced by TickSize (from *(double *)(DAT_0084247c + 0x50) + 8) |
0 |
FUN_00505d40:5923 (reset); FUN_005062e0:6241 |
FUN_005062e0:6240 |
_DAT_0084279c |
(pairs with 0x842798 as high half of double) | — | 0 |
FUN_00505d40:5924 |
(via 0x842798 as double) |
_DAT_008427a0 |
8 B (double) | Next sky-keyframe update deadline — compared to _DAT_008379a8, advanced by LightTickSize (from *(double *)(DAT_0084247c + 0x50) + 0x10) |
0 |
FUN_00505d40:5925; FUN_005062e0:6285,6289 |
FUN_005062e0:6249 |
_DAT_008427a4 |
(pairs as high half of 0x8427a0) | — | 0 |
FUN_00505d40:5926 |
— |
DAT_008427a8 |
1 B (bool) | FixedLight override enable — true means "run live keyframe interp each frame"; false means the caller of FUN_00505f30 supplies the values directly |
0 |
FUN_00505d40:5922 (set from param_1 != 0) |
FUN_00505f30:6043 |
DAT_008427a9 |
1 B (bool) | Crossfade in progress flag — enables blending from the previous keyframe output into the new one over _DAT_007c7208 per-frame step |
— | chunk_00500000.c:7270 |
FUN_005062e0:6256, 6297; chunk_00500000.c:7281 |
DAT_008427ac |
4 B (float) | Crossfade target: fog start (previous frame's fog start, held for lerp) | — | — | FUN_005062e0:6258, 6279 |
DAT_008427b0 |
4 B (float) | Crossfade target: fog start/secondary-1 | — | — | FUN_005062e0:6299, 6320 |
DAT_008427b4 |
4 B (float) | Crossfade target: fog start/secondary-2 | — | — | FUN_005062e0:6300, 6321 |
_DAT_008427b8 |
4 B (float) | Crossfade u-parameter — 0..1; advanced by _DAT_007c7208 per sample |
— | FUN_005062e0:6280, 6322 |
FUN_005062e0:6257, 6279, 6298, 6320 |
DAT_00842950 |
4 B (float) | Sun direction X | 0x3f99999a = 1.2f (default before first interp) |
FUN_00505f30:6044; FUN_00501600 → param_4[0] |
chunk_00500000.c:6057,6067,6058; chunk_00450000.c:4086,4337,4464; chunk_00530000.c:2029,2118,2137,2156,2175,2206 |
DAT_00842954 |
4 B (float) | Sun direction Y | 0 |
FUN_00505f30:6045; FUN_00501600 → param_4[1] |
same file set; appears as local_44[0] in terrain lighting |
DAT_00842958 |
4 B (float) | Sun direction Z | 0x3f000000 = 0.5f |
FUN_00505f30:6046; FUN_00501600 → param_4[2] |
same file set; appears as local_44[1] in terrain lighting |
DAT_0084295c |
4 B (float) | Minimum fog-start clamp — if interp result < this, snap up | — | (loaded from region config, probably MinWorldFog) | FUN_00505f30:6051-6052; FUN_005062e0:6253-6254 |
DAT_00842960 |
4 B (int) | Heightmap dim counter (used alongside DAT_00842790) | — | FUN_00500000.c:6544, 6540 |
— |
D3D light/material slots written from the block:
| D3D slot | Source | Written at |
|---|---|---|
_DAT_008682bc/c0/c4 |
fVar1/2/3 = copy of DAT_00842950/54/58 (a second sun-dir-like slot — likely "light direction copy") |
FUN_00505f30:6062-6064 |
_DAT_008682c8/cc/d0 |
DAT_00842950/54/58 directly (primary sun direction vector3) |
FUN_00505f30:6058-6060 |
DAT_008682d4 |
0 (light enable flag) |
FUN_00505f30:6065 |
2. Keyframe Interpolator — FUN_00501600 (0x00501600)
Signature:
void FUN_00501600(float param_1, // u = dayFraction (0..1)
float *param_2, // out: interpolated fog start scalar
undefined1 *param_3, // out: interpolated fog color (4 bytes ARGB)
float *param_4, // out: interpolated sun direction vec3 (scaled by brightness)
undefined1 *param_5); // out: interpolated ambient color (4 bytes ARGB)
The function first brackets u against the keyframe table (FUN_00501530, see §3), then interpolates.
Full decompile (chunk_00500000.c:1151-1232):
void FUN_00501600(float param_1,float *param_2,undefined1 *param_3,float *param_4,
undefined1 *param_5)
{
...
iVar5 = FUN_00501530(param_1,&local_14,&local_10,¶m_1);
uVar2 = local_10; // next keyframe
if (iVar5 != 0) {
// fog-start scalar: lerp field +0x14
*param_2 = (*(float *)(local_10 + 0x14) - *(float *)(local_14 + 0x14)) * param_1
+ *(float *)(local_14 + 0x14);
// fog color ARGB (3 bytes) — each byte is (lerp between k_i[0x18..0x1a] and k_i+1[0x18..0x1a])
// FUN_005df4c4 is a clamp-to-byte helper
param_2 = (float *)(uint)*(byte *)(local_10 + 0x19); // G
uVar3 = FUN_005df4c4();
param_2 = (float *)(uint)*(byte *)(local_10 + 0x18); // R
uVar4 = FUN_005df4c4();
local_10 = (uint)*(byte *)(local_10 + 0x1a); // B
param_2 = (float *)CONCAT31(param_2._1_3_,uVar4);
uVar4 = FUN_005df4c4();
param_3[2] = uVar4; // B
param_3[1] = uVar3; // G
*param_3 = param_2._0_1_; // R
param_3[3] = 0xff; // A
// Sun direction — polar (heading, pitch) w/ magnitude scalar (field +0x04)
fVar9 = ((float10)*(float *)(uVar2 + 4) - (float10)*(float *)(local_14 + 4)) * (float10)param_1
+ (float10)*(float *)(local_14 + 4); // fVar9 = magnitude (DirBright/length)
fVar1 = ((*(float *)(uVar2 + 0xc) - *(float *)(local_14 + 0xc)) * param_1 +
*(float *)(local_14 + 0xc)) * (float)_DAT_0079c6b0; // pitch_rad
fVar6 = (((float10)*(float *)(uVar2 + 8) - (float10)*(float *)(local_14 + 8)) * (float10)param_1
+ (float10)*(float *)(local_14 + 8)) * (float10)_DAT_0079c6b0; // yaw_rad
fVar7 = (float10)fcos((float10)fVar1); // cos(pitch)
fVar8 = (float10)fsin(fVar6); // sin(yaw)
local_c = (float)(fVar9 * fVar8 * fVar7); // x = mag * sin(yaw)*cos(pitch)
fVar6 = (float10)fcos(fVar6); // cos(yaw)
*param_4 = local_c;
local_8 = (float)(fVar9 * fVar6 * fVar7); // y = mag * cos(yaw)*cos(pitch)
fVar6 = (float10)fsin((float10)fVar1); // sin(pitch)
param_4[1] = local_8;
local_4 = (float)(fVar6 * fVar9); // z = mag * sin(pitch)
param_4[2] = local_4;
// Ambient color ARGB (3 bytes) — lerp field +0x10..0x12
param_2 = (float *)(uint)*(byte *)(uVar2 + 0x11); // G
uVar3 = FUN_005df4c4();
param_2 = (float *)(uint)*(byte *)(uVar2 + 0x10); // R
uVar4 = FUN_005df4c4();
param_3 = (undefined1 *)(uint)*(byte *)(uVar2 + 0x12); // B
...
param_5[2] = uVar4; // B
param_5[1] = uVar3; // G
*param_5 = param_2._0_1_; // R
param_5[3] = 0xff; // A
return;
}
// Fallback: no keyframe → white ambient, white fog, sun (0.5, 0, 0.8), fog_start 0.3
*param_2 = 0.3;
param_3[2] = 0xff; param_3[1] = 0xff; *param_3 = 0xff; param_3[3] = 0xff;
param_5[2] = 0xff; param_5[1] = 0xff; *param_5 = 0xff; param_5[3] = 0xff;
*param_4 = 0.5; param_4[1] = 0.0; param_4[2] = 0.8;
return;
}
Keyframe struct layout (inferred from FUN_00501600 + the loader/saver FUN_00501a20/FUN_00501b20 at chunk_00500000.c:1316-1446):
| Offset | Type | Semantic |
|---|---|---|
+0x00 |
float * (back-pointer to this) |
self |
+0x04 |
float |
t0 / Begin — day-fraction at which this keyframe is active (compared in bracket search) AND reused as DirBright (sun magnitude) scalar inside the interp! Actually this is DirBright per ACE schema; bracket search at FUN_00501530:1115 uses *(float *)*puVar4 which dereferences the back-pointer — so the actual t0 lives at *this (offset 0), and the field at +0x04 is DirBright. But the save-order in FUN_00501a20 starts from +0x04, so the on-disk file does NOT store t0 explicitly here — it's elsewhere (the outer container holds t0; see §3). |
+0x08 |
float |
DirHeading (degrees, converted to radians via _DAT_0079c6b0 = π/180) |
+0x0c |
float |
DirPitch (degrees → radians) |
+0x10..+0x12 |
3×byte |
AmbColor (R, G, B at +0x10, +0x11, +0x12) |
+0x13 |
byte |
AmbColor alpha (unused; always 0xff on write) |
+0x14 |
float |
MinWorldFog / fog start scalar (or AmbBright; see §7 gaps) |
+0x18..+0x1a |
3×byte |
FogColor (R, G, B) |
+0x1b |
byte |
FogColor alpha |
+0x1c |
ptr |
Optional per-keyframe mesh/sky-object chain (non-zero used by FUN_00501860) |
+0x20 |
float |
Secondary field 1 (ACE calls this DirColor.Luminosity or MaxWorldFog; used in FUN_00501860:1250) |
+0x24 |
float |
Secondary field 2 (used in FUN_00501860:1252 — pairs with +0x20) |
+0x28..+0x2a |
3×byte |
Secondary color (used in FUN_00501860:1254-1263) |
+0x2b |
byte |
Secondary color alpha |
Total struct size: 0x2c bytes (44 B), which matches the FUN_005df0f5(0x2c) alloc in chunk_00500000.c:5972. This is the in-memory SkyTimeOfDay (one keyframe).
The outer keyframe-table header is at param_1 in FUN_00501530:
*(int *)(param_1 + 8)= pointer to array of keyframe pointers*(int *)(param_1 + 0x10)= keyframe count*(float *)*puVar4= keyframe'st0(reached via the back-pointer at struct +0x00)
3. Keyframe bracket search — FUN_00501530 (0x00501530)
chunk_00500000.c:1093-1129:
undefined4 __thiscall
FUN_00501530(int param_1,float param_2,undefined4 *param_3,undefined4 *param_4,float *param_5)
{
iVar1 = *(int *)(param_1 + 0x10); // count
if (iVar1 == 0) return 0;
uVar3 = 0;
if (iVar1 != 1) {
puVar4 = *(undefined4 **)(param_1 + 8); // array base
do {
puVar4 = puVar4 + 1;
if (param_2 < *(float *)*puVar4) break; // u < keyframe[i+1].t0 ?
uVar3 = uVar3 + 1;
} while (uVar3 < iVar1 - 1U);
}
*param_3 = *(undefined4 *)(*(int *)(param_1 + 8) + uVar3 * 4); // lower keyframe
if (uVar3 == *(int *)(param_1 + 0x10) - 1U) {
*param_4 = **(undefined4 **)(param_1 + 8); // wrap to keyframe[0]
*param_5 = (param_2 - *(float *)*param_3) / (_DAT_007938b0 - *(float *)*param_3);
// u01 = (t - lower.t0) / (1.0 - lower.t0) — wrap-around case
return 1;
}
pfVar2 = (float *)(*(undefined4 **)(param_1 + 8))[uVar3 + 1];
*param_4 = pfVar2; // upper keyframe
*param_5 = (param_2 - *(float *)*param_3) / (*pfVar2 - *(float *)*param_3);
// u01 = (t - lower.t0) / (upper.t0 - lower.t0)
return 1;
}
This is exactly the interpolator-finder pattern the hunt asked for. The t0 for each keyframe is stored at the FIRST FIELD of each keyframe record (via the back-pointer layer). The last keyframe wraps to keyframe[0] using 1.0 as the upper bound (_DAT_007938b0 = 1.0).
4. Sun direction formula — confirmed
chunk_00500000.c:1192-1205 computes the sun unit vector from (DirHeading, DirPitch):
yaw_rad = DirHeading * π/180
pitch_rad = DirPitch * π/180
mag = DirBright (interpolated, scales the output length)
x = mag * sin(yaw) * cos(pitch)
y = mag * cos(yaw) * cos(pitch)
z = mag * sin(pitch)
This matches the hunt-brief formula exactly except it multiplies by the per-keyframe magnitude fVar9 (DirBright). So DAT_00842950/54/58 is not a unit vector; it is DirBright * unit_direction. The terrain lighting code at chunk_00530000.c:2118 dot-products this with per-vertex normals, so the DirBright scaling baked into the vector directly modulates the diffuse lighting strength.
Coordinate frame: retail AC is Y-up world for sky azimuth but stores sun in world coords where Z-axis is vertical (common in retail AC: X=east, Y=north, Z=up). sin(pitch) → Z means pitch=+90° → straight up; pitch=0 → horizontal; that matches AC's "degrees above horizon" convention.
5. Day-fraction / tick source
chunk_00500000.c:6239-6248 (inside FUN_005062e0):
local_14 = _DAT_008379a8; // game-clock (seconds, double)
if ((_DAT_00842798 <= _DAT_008379a8) && (DAT_0084247c != 0)) { // cloud-deadline met & region loaded
_DAT_00842798 = *(double *)(*(int *)(DAT_0084247c + 0x50) + 8) + _DAT_008379a8;
// ^--- SkyDesc.TickSize (from the Region DB object)
if (DAT_008ee9c8 == 0) {
fVar1 = (float)_DAT_00795610; // = 0.0f (no world loaded)
}
else {
fVar1 = *(float *)(DAT_008ee9c8 + 0x48); // <-- THIS is the dayFraction
}
And at chunk_00500000.c:6285-6289:
_DAT_008427a0 = _DAT_007c7200; // default LightTickSize
if (DAT_0084247c != 0) {
_DAT_008427a0 = *(double *)(*(int *)(DAT_0084247c + 0x50) + 0x10);
// ^--- SkyDesc.LightTickSize (from Region DB object)
}
_DAT_008427a0 = _DAT_008427a0 + local_14; // advance deadline
Day-fraction source: *(float *)(DAT_008ee9c8 + 0x48).
DAT_008ee9c8 is the world/time global (a CTimeStamp-like struct). Its fields used by the sky:
+0x10: int — "tickCount A" component (used inFUN_00501990:1293, a PRNG seed hint)+0x48: float dayFraction (0..1) — directly consumed by FUN_00501600 asparam_1+0x64: int — "tickCount B"+0x68: int — "tickCount C"
I did NOT locate the writer of +0x48 (the conversion from wall-clock seconds to dayFraction) in this hunt — see §7 "Gaps".
Region-config pointer: DAT_0084247c points at the loaded Region object; Region + 0x50 is a pointer into SkyDesc, and SkyDesc + 8 = TickSize (double seconds) and SkyDesc + 0x10 = LightTickSize (double seconds) — matching the dat schema (docs/research/2026-04-23-sky-dat-schema.md:21-22).
6. Per-frame sky update entry point — FUN_005062e0 (0x005062E0)
The function the hunt brief is asking for. chunk_00500000.c:6213-6333.
Skeleton (key flow, comments added):
void __fastcall FUN_005062e0(int param_1)
{
...
if (*(int *)(param_1 + 0x10) != 0) {
if (*(int *)(param_1 + 0x20) != 0) {
FUN_00508010(); // prepare cloud buffers
}
local_14 = _DAT_008379a8; // game-clock snapshot
if ((_DAT_00842798 <= _DAT_008379a8) && (DAT_0084247c != 0)) {
_DAT_00842798 = SkyDesc.TickSize + _DAT_008379a8; // next tick deadline
fVar1 = (DAT_008ee9c8 == 0) ? 0.0f : *(float *)(DAT_008ee9c8 + 0x48); // dayFraction
// ---- Sky-keyframe update (gated by LightTickSize) ----
if (_DAT_008427a0 < _DAT_008379a8) {
iVar7 = FUN_004ff440(fVar1,&local_24,&local_20,local_c,&local_18);
// ^ primary interp: fogStart, fogColorARGB, sunDirVec3, ambColorARGB
if (iVar7 != 0) {
if (local_24 < DAT_0084295c) local_24 = DAT_0084295c; // clamp min fog start
if (DAT_008427a9 != '\0') { // crossfade active
// blend towards previous frame's held values (DAT_008427ac, DAT_00842788)
// step crossfade u by _DAT_007c7208 each tick
...
}
FUN_00505f30(local_24, local_20, local_c, local_18);
// ^ commits the interp result to the DAT_008427xx / DAT_008429xx globals
// AND pushes them to D3D light/material/fog state.
}
_DAT_008427a0 = SkyDesc.LightTickSize + local_14; // advance light deadline
}
// ---- Secondary interp (starfield / alt palette — DAT_00842784/88) ----
FUN_005a4010(DAT_0081dbf8 == '\0');
if (DAT_0081dbf8 != '\0') {
FUN_005a3f90(DAT_0081dbf8);
iVar7 = FUN_004ff480(fVar1,&local_1c,&local_24,&local_20);
// ^ secondary interp (starfield): 2 floats + 1 color
if (iVar7 != 0) {
// same crossfade pattern targeting DAT_008427b0/b4 and DAT_00842784
...
FUN_005a41b0(&local_20, local_1c, local_24); // push to starfield state
}
}
}
}
}
Entry-point contract:
- Called once per world/frame tick from the top-level world update (see §7).
- Reads
DAT_008ee9c8 + 0x48for the current dayFraction. - Runs two independent interpolators against the same keyframe tables, throttled by
TickSize/LightTickSize(fromSkyDesc). FUN_00505f30(see below) writes the final values into the globals that the rest of the render pipeline samples.
6b. Commit-to-globals — FUN_00505f30 (0x00505F30)
chunk_00500000.c:6026-6092. This is the function that writes the DAT_00842xxx globals. It is the public "set sky state" entry point — called with either (a) fresh interp output (from FUN_005062e0:6283) or (b) caller-supplied values (the FixedLight override path when DAT_008427a8 == 0).
void FUN_00505f30(int param_1, float param_2, undefined4 param_3,
float *param_4, undefined4 param_5)
{
DAT_00842780 = param_2; // fog start
DAT_0084277c = param_3; // fog color ARGB
if (DAT_008427a8 == '\0') { // FixedLight mode: caller provides raw values
DAT_00842950 = *param_4; // sun X
DAT_00842954 = param_4[1]; // sun Y
DAT_00842958 = param_4[2]; // sun Z
DAT_00842778 = param_5; // ambient color ARGB
}
else { // dynamic mode: re-run interp against current dayFrac snapshot
iVar4 = FUN_004ff440(0x3f000000, // 0.5 placeholder (?)
&DAT_00842780, &DAT_0084277c,
&DAT_00842950, &DAT_00842778);
if ((iVar4 != 0) && (DAT_00842780 < DAT_0084295c)) {
DAT_00842780 = DAT_0084295c; // min fog clamp
}
}
// Push sun direction to the active D3D directional light
fVar3 = DAT_00842958; fVar2 = DAT_00842954; fVar1 = DAT_00842950;
_DAT_008682c8 = DAT_00842950;
_DAT_008682cc = DAT_00842954;
_DAT_008682d0 = DAT_00842958;
// Unpack ambient color ARGB → 3 floats into the D3D material
FUN_00451a60(DAT_00842778);
_DAT_008682bc = fVar1; _DAT_008682c0 = fVar2; _DAT_008682c4 = fVar3;
DAT_008682d4 = 0;
// Fog: distance = length(sunDir) * 0079a1e8 + fogStart; color = fog color ARGB
if (DAT_0083da58 != 0) {
FUN_004530e0(SQRT(DAT_00842950 * DAT_00842950 +
DAT_00842954 * DAT_00842954 +
DAT_00842958 * DAT_00842958) * _DAT_0079a1e8 +
DAT_00842780,
DAT_0084277c);
}
// Touch each dungeon EnvCell (re-apply fog/lighting state to each)
if (*(int *)(param_1 + 8) != 0) {
... FUN_00532440(); ...
}
}
Dispatch glue — FUN_004ff440 (at chunk_004F0000.c:10692-10704) is a null-check wrapper around FUN_00501600:
undefined4 FUN_004ff440(param_1, param_2, param_3, param_4, param_5) {
if ((DAT_0084247c != 0) && (*(int *)(DAT_0084247c + 0x50) != 0)) {
FUN_00501600(param_1, param_2, param_3, param_4, param_5);
return 1;
}
return 0;
}
7. Call graph
FUN_004554b0 (0x004554B0 — world update root) chunk_00450000.c:3831
└── FUN_005062e0 (0x005062E0 — per-frame sky update) chunk_00500000.c:6213
├── reads: _DAT_008379a8 (game-clock)
│ DAT_008ee9c8 + 0x48 (dayFraction)
│ DAT_0084247c + 0x50 (SkyDesc ptr)
│ DAT_0084247c + 0x50 + 8 (TickSize double)
│ DAT_0084247c + 0x50 + 0x10 (LightTickSize double)
├── writes: _DAT_00842798 (next cloud tick)
│ _DAT_008427a0 (next light tick)
│ _DAT_008427b8 (crossfade u)
│ DAT_008427a9 (crossfade active)
├── FUN_004ff440 → FUN_00501600 (primary interp) chunk_00500000.c:1151
│ └── FUN_00501530 (bracket search) chunk_00500000.c:1093
├── FUN_00505f30 (commits globals + D3D state) chunk_00500000.c:6026
│ ├── writes: DAT_00842778, DAT_0084277c, DAT_00842780
│ │ DAT_00842950, DAT_00842954, DAT_00842958
│ ├── writes: _DAT_008682bc..d4 (D3D directional light)
│ ├── FUN_00451a60 (unpack ambient ARGB → 3 floats) chunk_00450000.c:608
│ └── FUN_004530e0 (push fog state)
└── FUN_004ff480 → FUN_00501860 (secondary interp: starfield) chunk_00500000.c:1236
└── FUN_00505f30-equivalent callers use DAT_00842784/88
--- Readers (consumers of sky globals per frame) ---
chunk_00450000.c:4086,4337-4341,4464-4468 // landblock draw re-applies to D3D
chunk_00450000.c:4347, 4474 // landblock draw re-applies fog color
chunk_00530000.c:2094-2230 // per-vertex terrain shading:
// diffuse = max(dot(vertexNormal, sunDir), minAmbient)
// lit_color = ambRGB*fogStart + diffuse*fogRGB
// clamp to 1.0 per channel
Note: chunk_00530000.c:2094-2230 is the canonical per-vertex lit-color calculation (retail's CPU lighting path). It consumes:
DAT_00842950/54/58as the sun direction (NOT normalized — carriesDirBrightbaked in).DAT_00842780as the "ambient base multiplier" (namedDirBrightin the schema but semanticallyAmbBrighthere: it's the scalar multiplying the ambient ARGB, not the sun).DAT_00842778ARGB → (R,G,B)/255 as ambient color (confirmed: line 2100,2104,2105).DAT_0084277cARGB → (R,G,B)/255 as diffuse color (confirmed: line 2095,2097,2099).DAT_00796344= minimum-dot clamp (avoids fully-black back faces)._DAT_007938c0(probably 1.0 or slightly above) = per-channel saturation clamp.
This flips my §1/§2 preliminary labels: DAT_0084277c is actually the DIFFUSE/sun color (not fog) and DAT_00842778 is the AMBIENT color. The FUN_004530e0 call at FUN_00505f30:6067-6069 passes DAT_0084277c to the fog setup, but fog in retail AC also takes the diffuse/sun color as its tint — that's the known design (distant fog matches sun tint). So the semantic naming that unifies both consumers is:
DAT_0084277c= DirColor (sun/diffuse ARGB) — doubles as fog tintDAT_00842778= AmbColor (ambient ARGB)DAT_00842780= scalar brightness mul applied to the AMBIENT in the lit-color formula, AND as the fog-start offset (it's a dual-purpose "ambient base" scalar)
This matches the FUN_00501600 output mapping from §2:
- interp out
*param_2← keyframe+0x14→DAT_00842780→ "ambient scalar / fog base" - interp out
*param_3← keyframe+0x18..+0x1a→DAT_0084277c→ DirColor (sun + fog) - interp out
*param_4[0..2]← keyframe+0x04,+0x08,+0x0cvia polar→cartesian →DAT_00842950..58→ sun direction * DirBright - interp out
*param_5← keyframe+0x10..+0x12→DAT_00842778→ AmbColor
So the on-disk SkyTimeOfDay→in-memory field offsets are:
| Offset | Field (ACE schema name) | Matched consumer |
|---|---|---|
+0x04 |
DirBright |
sun-vector magnitude fVar9 |
+0x08 |
DirHeading (deg) |
yaw → sin/cos |
+0x0c |
DirPitch (deg) |
pitch → cos/sin |
+0x10/+0x11/+0x12 |
AmbColor (R,G,B) |
DAT_00842778 |
+0x14 |
AmbBright (!) |
DAT_00842780 — multiplies AmbColor in lit_color; also fog-start offset |
+0x18/+0x19/+0x1a |
DirColor (R,G,B) |
DAT_0084277c — diffuse AND fog tint |
+0x20 |
MinWorldFog |
secondary interp local_1c via FUN_00501860:1250 |
+0x24 |
MaxWorldFog |
secondary interp local_24 via FUN_00501860:1252 |
+0x28/+0x29/+0x2a |
WorldFogColor (R,G,B) |
starfield ARGB in FUN_005a41b0 |
This matches exactly the published ACE SkyTimeOfDay struct (docs/research/2026-04-23-sky-dat-schema.md:42-54) — every field has a decompile home now.
8. Gaps / next hunts
-
Day-fraction writer. I found
*(float *)(DAT_008ee9c8 + 0x48)is READ asdayFraction. I did NOT locate the WRITER — i.e. which function computesdayFraction = (gameClockSeconds mod dayLength) / dayLengthand stamps it to+0x48. Look in chunk_004F0000.c forDAT_008ee9c8 + 0x48write-sites, likely driven by a world-tick function usingSkyDesc.TickSize. -
DAT_00796344and_DAT_007938c0constants. In the per-vertex lighting (chunk_00530000.c:2119),DAT_00796344is the minimum-dot clamp._DAT_007938c0is the per-channel saturation clamp. Both should be extracted to confirm they are0.0fand1.0frespectively (or something like0.0and> 1.0to allow HDR). -
FixedLight (static sky) path. When
DAT_008427a8 == 0(FixedLight mode), the caller supplies raw sun direction, ambient, fog color. This is likely the path used for dungeons (no day/night cycle). The caller ofFUN_00505d40(0)(which setsDAT_008427a8 = 0) needs to be mapped — it's the dungeon-load entry. -
Starfield secondary keyframe.
FUN_00501860/FUN_005a41b0(starfield state) uses keyframe fields+0x20, +0x24, +0x28..+0x2a. If acdream renders stars, portFUN_005a41b0too. -
Crossfade parameters
_DAT_007c7208andDAT_008427ac. The crossfade (DAT_008427a9) is a smoothing blend when keyframes are force-swapped (e.g. weather change)._DAT_007c7208is the per-tick u-step;DAT_008427acis the held previous value. If acdream wants faithful weather transitions, port this loop fromFUN_005062e0:6256-6281. -
Fog calling convention
FUN_004530e0. I did not decompile this but its signature —FUN_004530e0(fogDistance, fogColorARGB)— is unambiguous. Port asSetFog(distance, ARGB). Thedistancearg is|sunDir| * _DAT_0079a1e8 + fogStart— so distant fog grows with the "weighted sun-vector length". This is AC's recognizable fog-near-horizon effect. -
The already-known globals hint resolved. The hunt brief cited
DAT_00842778/7c,DAT_00842950..58— all mapped in §1. No other sky-specific globals exist in this address neighborhood.
9. Quick summary for the porter
What the retail sky pipeline does per frame:
- Pull dayFraction from
world.time.dayFraction(DAT_008ee9c8 + 0x48, a float 0..1). - Gate on
LightTickSizeelapsed — only interpolate everyLightTickSizeseconds; otherwise reuse last frame's values (hence the visible ~2-second "step" in AC's time-of-day tint). - Bracket dayFraction across the current DayGroup's
SkyTimeOfDay[]byt0. Compute u01. - Interpolate each channel: fogStart, fogColorARGB (byte-lerp), sunDir via polar-to-cartesian of (DirHeading, DirPitch) scaled by DirBright, ambColorARGB (byte-lerp).
- Clamp fogStart ≥
DAT_0084295c(the MinWorldFog floor from the Region). - Crossfade blend if a keyframe swap was forced (weather).
- Commit to six floats (sunDir x3, fogStart x1) + two ARGB dwords (fog=sun, amb).
- Push those values to D3D:
_DAT_008682bc..d0= sun vec3 duplicated into "direction copy" and "direction primary" slots;_DAT_008682c8..d0= sun vec3 copy 2; a material ambient RGB (unpacked from the ARGB); and fog(distance, color). - Re-apply the state on every landblock/EnvCell draw via
FUN_00451a60+FUN_004530e0(because D3D resets between passes).
All values to port live in DAT_008427xx / DAT_00842950..58 — no secrets elsewhere.