# 2026-04-28 Sky Cloud Material Trace

Context: Phase C.1 originally treated the Rainy/Cloudy sky visual as a
SkyObject PES problem. Retail named-decomp and dat inspection disprove that
for the broad cloud/ray layer.

## Retail Trace

- `LScape::draw` (`0x00506330`) calls `GameSky::Draw(0)` before terrain and
  `GameSky::Draw(1)` after terrain.
- `SkyDesc::GetSky` copies `pes_id`, but `GameSky::CreateDeletePhysicsObjects`
  compares/replaces only `gfx_id` and calls `GameSky::MakeObject(gfx_id, ...)`.
  The sky object PES id is not part of retail `GameSky` rendering.
- `GameSky::UseTime` applies keyframe replace fields to instantiated sky
  objects:
  - `0x005076e1`: `CPhysicsObj::SetLuminosity(luminosity * 0.01)`
  - `0x00507715`: `CPhysicsObj::SetDiffusion(max_bright * 0.01)`
  - `0x00507747`: `CPhysicsObj::SetTranslucency(transparent * 0.01)`
- `CMaterial::SetTranslucencySimple` (`0x005396f0`) writes material alpha as
  `1 - translucency`.
- `CMaterial::SetDiffuseSimple` (`0x00539750`) writes material diffuse RGB.
  Therefore `SkyObjectReplace.MaxBright` is diffuse, not an emissive cap.
- `D3DPolyRender::SetSurface` (`0x0059c4d0`) disables fixed-function fog alpha
  whenever the raw `SurfaceType.Additive` bit is set (`0x0059c882`), even when
  the earlier `Translucent + ClipMap` branch forces normal alpha blending.

## Dat Trace

The broad Rainy/Cloudy layer is `GfxObj 0x01004C35`, not one of the tiny
`0x020xxxxx` setup anchors:

- `0x01004C35`: huge sky mesh, bbox roughly `20175 x 20175 x 1180`, UVs tile
  across the sheet.
- Surface `0x08000023`: `Base1ClipMap | Translucent | Alpha | Additive`
  (`0x00010114`), `Translucency=0.25`, `Luminosity=0`, `Diffuse=1`.
- Texture `0x060037AF`: 256x256 A8R8G8B8 cloud/ray texture.

The setup ids observed in Rainy groups (`0x02000588`, `0x02000589`,
`0x02000BA6`, `0x02000714`) are one-part dummy anchors with tiny `0x010001EC`
geometry and default scripts/PES for sounds/flashes. They are not the broad
cloud layer.

## Port Consequences

- Keep per-SkyObject PES rendering debug-only until another retail path proves
  it is used.
- Render `0x08000023` as final alpha blend because retail's translucent/clipmap
  branch overrides the raw additive blend.
- Still disable sky fog for that surface because retail keys fog-alpha disable
  off the raw `Additive` bit.
- Route `MaxBright` to diffuse (`uDiffuseFactor`) and `Luminosity` to emissive.
- Use a final opacity multiplier for material/surface transparency before the
  fragment alpha write; dynamic keyframe transparency remains `1 - value`.

## WorldBuilder Cross-Check

Cloned upstream `https://github.com/Chorizite/WorldBuilder.git` at commit
`167788be6fce65f5ebe79eef07a0b7d28bd7aa81`. Its
`Chorizite.OpenGLSDLBackend/Lib/SkyboxRenderManager.cs` renders sky objects
camera-centered with depth off, but it is not a faithful retail oracle for sky
tint: `GameScene.cs` has the skybox render call commented out, the manager
always selects `DayGroups[0]`, and it uploads `SunlightColor = Vector3.Zero`
/ `AmbientColor = Vector3.One` for sky. `RegionInfo.cs` interpolates
DayGroup[0] lighting for terrain/world objects, not the active retail
DayGroup/weather sky.

That explains why WorldBuilder cannot answer the missing green/purple Rainy
sky tint directly. The actionable lesson is narrower: do not fog-paint the
raw-additive cloud sheet itself. In acdream, non-additive sky layers now receive
the keyframe fog tint so the broad background wash appears behind clouds, while
surfaces with the raw Additive bit (notably `0x08000023`) keep fixed-function
fog disabled and preserve the pink cloud/ray detail.

WorldBuilder's regular object path does collect `Setup.DefaultScript`
particle hooks (`ObjectMeshManager.CollectEmittersFromScript`) and instantiates
them via `ObjectRenderManagerBase`, but its skybox manager does not use that
setup/particle path for SkyObjects. Dat inspection also showed the canonical
Rainy default script target `0x3300042C` is a sound-loop chain (`SoundTweaked`
+ `CallPES`), not the broad green tint or cloud ray layer.

Additional renderer lessons from upstream WorldBuilder:

- Particle blend is material-derived. `ParticleEmitterInfo` does not carry an
  additive flag; WorldBuilder reads `ObjectRenderData.Batches[0].IsAdditive`
  from the particle GfxObj surface. acdream now leaves DAT emitters non-additive
  by default and resolves particle blend from the selected particle surface.
- Particles must be globally sorted back-to-front before drawing. Sorting only
  inside per-texture dictionaries can reorder translucent particles whenever
  multiple textures/blend states are active.
- Particle quads come from the authored particle GfxObj bounds. Degenerate
  extents fall back to `1.0`, and point-sprite degrade mode applies a `0.9`
  base scale.
- Texture decoding must try highres `RenderSurface` records after portal lookup
  and must zero alpha for black pixels on compressed clipmap textures.
- WorldBuilder tracks UV wrap and cull mode per object batch. acdream's sky path
  already uses authored UV wrap, but shared object rendering still needs the
  same metadata carried through a later C.4 pass.
