# Time of day

Hikari treats a dynamic sky as one environment source, not as a sequence of
skybox swaps. `AtmosphereSettings` drives the visible clear sky, diffuse environment lighting,
specular environment fallback, GI sky misses, reflections, and raster/ray-traced
misses from the same state.

## Runtime controller

The scene Environment block owns the authored starting clock, calendar,
location, and world-north frame. A gameplay controller reads it at `Start`,
copies `time_of_day` into runtime/save state, then derives solar position once
per simulation update for both the directional light and atmosphere:

```zig
const authored = hi.render().atmosphereClock();
if (starting_new_game and authored.available != 0)
    time_of_day = authored.time_of_day;

const solar = hi.atmosphere.solarPosition(.{
    .hour = time_of_day,
    .day_of_year = @intCast(authored.day_of_year),
    .latitude_degrees = authored.latitude_degrees,
    .longitude_degrees = authored.longitude_degrees,
    .utc_offset_hours = authored.utc_offset_hours,
});

hi.world().setRotationEuler(sun, solar.lightEuler());

var sky = hi.render().atmosphereState();
if (sky.enabled == 0) sky = hi.AtmosphereSettings.earth();
sky.sun_direction = solar.direction;
sky.moon_direction = hi.atmosphere.oppositeCelestialDirection(solar.direction);
hi.render().setAtmosphere(sky);

// Ground-level sunlight: the profile's `sun_color` is the top-of-atmosphere
// reference, and this is what survives the column the sun is shining through.
hi.render().setLight(sun, .{ .color = hi.atmosphere.sunlightColor(sky) });
```

The directional light owns direct solar energy and shadows. The atmosphere's
sun disk is visual only and is excluded from diffuse environment lighting and specular IBL, so it
does not double-light surfaces. A shadowless moon directional is optional;
dynamic atmosphere lighting already provides broad night illumination.

### Sunset is extinction, not a colour ramp

`sun_color` and `moon_color` are the bodies' colours *above* the atmosphere.
What reaches the ground is that colour after the optical column the body is
currently shining through, and `hi.atmosphere.sunlightColor` /
`moonlightColor` return exactly that: warm white overhead, deep red and heavily
dimmed at the horizon, near black once the body sets. `hi.atmosphere.transmittance`
exposes the same column for any direction.

Applying it to the light is what keeps the light and the sky consistent — the
visible sun disk is extinguished through the same column, so the disk in the
sky and the light casting the shadows redden together. Do not reach the same
look by pushing a reddened colour into `sky.sun_color`: that is the
extraterrestrial reference the whole sky is derived from, so tinting it turns
the entire hemisphere red instead of just the low sun.

The light's **intensity** stays authored. It is the extraterrestrial scale; all
of the day's dimming is already in the per-channel extinction, so scaling
intensity by a daylight curve as well double-counts it. Enabling and disabling
the light around the horizon is still the controller's call.

A scene that links its sun through the Environment panel gets all of this
without a script — see [Scene authoring and editor preview](/docs/guides/time-of-day.md#scene-authoring-and-editor-preview).

Starting from `atmosphereState()` preserves the scene-authored scattering,
color, disk, and aerial-perspective profile while the controller updates its
dynamic celestial fields. Falling back to `earth()` lets the same controller
explicitly switch a skybox scene to atmosphere. Do not rebuild Earth defaults
every tick in a scene whose Environment panel is intended to remain the source
of those settings.

`solarPosition` uses a deterministic NOAA fractional-year approximation. The
inputs are local civil time, day of year, latitude, longitude, and UTC offset;
they are deliberately independent of wall-clock time and therefore suitable for
save games, networking, replay, and accelerated demo days. If scene +Z is not
geographic north, rotate the returned direction around world +Y by
`authored.world_north_degrees` (the sample controllers do this). Only the
advancing clock belongs to runtime/save state; do not mutate the scene-authored
starting value. Keep runtime state on a scene/session controller, not in
process-global state: editor and Play can own separate `World` instances at the
same time.

Reference controller: `hikari-games/example/src/entities/sun_cycle_entity.zig` (the Sponza day/night cycle).

## Scene authoring and editor preview

The left-dock **Environment** panel has two deliberately separate sections.
**Scene Environment** edits the scene document and is saved, undoable, and
visible immediately in the Edit viewport. Choose **Atmosphere** or **Skybox**
as the active source. Both branches remain stored when switching, so comparing
an HDRI against a procedural sky does not destroy either setup.

Atmosphere authoring includes the scene's default civil time, calendar and
location, world-north correction, luminance and scattering profile, and aerial
perspective. Optional sun/moon light actor IDs rotate **and colour** the scene's
directional lights from the same astronomical result: the pose comes from the
clock, the colour is `sunlightColor` / `moonlightColor` for the resolved
profile. A linked light's authored colour is therefore ignored — the profile's
`sun_color` is the reference instead — while its authored intensity, shadow and
volumetric settings are left alone. That makes a complete static lighting setup,
sunset reddening included, possible without a script.

Skybox authoring accepts primary and secondary
HDR/EXR cubemaps, HDR tint/exposure, and blend. The scene root is applied after
actor IDs and transforms exist but before component `Start`, so a runtime
controller may intentionally layer motion and choreography over these defaults
without competing with an editor system every frame. `atmosphereClock()` and
`atmosphereState()` are read-only views of that handoff.

Agents author the same surface over MCP: `scene_get` reports `source`, `skybox.*`, `atmosphere.*`, and the profile under `atmosphere.settings.*`; `scene_edit`'s `set_environment` op writes one addressed field per op. Paths are the keys `scene_get` emits, because both walk the same reflected table (`editor/authoring/environment_fields.zig`), so a field added to `AtmosphereSettings` is scriptable the day it lands.

**Time Preview** is the transient authoring equivalent of a SunSky clock. Scrub
the 24-hour timeline, jump to **Sunrise**,
**Noon**, **Sunset** or **Midnight**, play or pause the day, restart it at
midnight, loop it, and pick a speed (0.25× / 1× / 4× / 16×, where 1× is one hour
per second). Any of those arms the override on its own; **Preview time of day**
is the release back to the scene's own clock, not a gate in front of the
controls. The transport reads out the hour and the phase it lands in
(Dawn, Morning, Midday, Afternoon, Sunset, Dusk, Night). The same panel may be
docked or detached like every other editor panel. It evaluates directly in the
Edit viewport; entering Play is not required.

Only the time override is transient editor state. It never changes the scene document and is not undoable. It is a **scoped override**: the first override write captures the World's environment as shown (sky, atmosphere, authored clock, linked light poses and colours), and ending the override restores that capture. The scope closes at the Play click, before the Play snapshot, so Stop returns to the pre-scrub look; a fresh scope opens on the first Edit tick afterwards. Any other writer moving the environment under an open scope (a scene load, an environment edit, the game) makes the scope stale, and a stale scope is dropped rather than restored, so a previewed hour can never become a new baseline and never erases a look the game set.

For a scriptless authored atmosphere, the transport resolves the scene's stored
astronomy directly and rotates its linked sun/moon lights. A game-specific
controller can opt into the same transport when it must preview a wider lighting
rig—practical lights, fog volumes, emissive materials, exposure, or weather—by
implementing the component lifecycle callback:

```zig
pub fn onEnvironmentPreview(
    self: *@This(),
    actor: hi.ActorContext,
    preview: hi.EnvironmentPreview,
) void {
    const authored = hi.render().atmosphereClock();
    const hour = if (preview.active) preview.time_of_day else authored.time_of_day;
    self.evaluateEnvironment(actor.id, hour);
}
```

Here `evaluateEnvironment` is your component’s helper that applies the light and sky state for an actor and hour.

Treat `preview.time_of_day` as an evaluator input, never assign it to authored
or save-game state. When `active` is false, evaluate the scene's authored clock
to restore the normal Edit look. Scene-environment edits automatically
re-evaluate the live World; changing an artistic controller curve in the
Inspector invalidates its current preview through `onChanged`.

A skybox scene with no opted-in controller shows an empty transport state: a
static cubemap has no celestial clock. An atmosphere scene always has the
scriptless astronomical preview above.

World indexes only opted-in entities and caches the last effective preview, so a stable editor frame performs no controller walk. Keep the callback free of structural mutation: evaluate transforms and rendering state, do not spawn or despawn.

## Production choreography

One solar elevation should drive normalized, overlapping curves rather than a
set of hour equality checks. A production controller derives daylight, night,
deep-night, horizon and morning factors, then applies those factors to the whole
scene contract:

- continuously update sun/moon transforms, direct energy and atmosphere;
- fade fixture light intensity and matching material emissive together, then
  disable the lights once their factor reaches zero so clustering and shadow
  work disappear during the day;
- reserve shadowed volumetric scattering for hero shaft lights. Unshadowed local
  scattering crosses walls and roofs and reads as a glowing sphere;
- drive exposure, grade, bloom and global height fog through Visual Zones, with
  higher-priority interior zones retaining spatial ownership of the look;
- animate local Fog Volumes through `density_scale`, not by replacing their
  material extinction. The scale is applied after the authored height/noise
  graph, so weather timing cannot destroy its spatial shape.

```zig
hi.render().setFogVolume(volume, .{
    .density_scale = morning_factor,
    .enabled = morning_factor > 0.01,
});
```

The sun and atmosphere are cheap, continuous state. Large practical-light,
material and fog rigs should publish only after their controlling factor changes
by a small perceptual threshold. Cache each light/material's authored baseline
once and multiply it; do not bake a second set of night values into engine code.
Speed controls should multiply a scene-authored base day length and clamp their
range, leaving save-game time and authoring data deterministic.

## Environment ownership

- The scene root `environment` is the persistent default and has one explicit
  active `source`. Both source branches are serialized so mode switching is
  nondestructive. Additive scene layers do not replace the owning world's
  environment.
- Runtime scripts use the same `World` setters and remain authoritative after
  scene startup. The authored environment is not re-applied each frame.
- Call `setAtmosphere(AtmosphereSettings.earth())` to select the procedural
  source. Primary/secondary cubemaps can remain empty; they are ignored while
  atmosphere is enabled.
- Call `setAtmosphere(AtmosphereSettings.disabled())` before selecting an
  authored cubemap environment.
- HDR/EXR cubemaps remain fully supported. Use the cubemap blend for authored
  environments and deliberately art-directed transitions. For an astronomical
  full-day cycle, the procedural source keeps sky, IBL, GI and reflections in
  one continuous state without maintaining and streaming a dense HDR sequence.
- There are no placed reflection probes. Past the reflection trace gate,
  interior specular leans on the GI gather instead of mirroring the sky; see
  [Rendering](/docs/systems/rendering.md#rendering-contracts-composability).
- Visual Zones still own exposure, fog and post grading. They do not own the
  celestial clock or environment radiance.

### How the sky is built

The sky is three small lookup tables and one environment cube (Hillaire 2020; `atmosphere.akari`, `atmosphere_lut.akari`). Every consumer reads that chain rather than evaluating the atmosphere itself, and none of it scales with render resolution.

| Resource | Texels | Rebuilt when |
| --- | --- | --- |
| Transmittance | 256 × 64 | the medium changes (profile edit) |
| Multiple scattering | 32 × 32 | the medium changes |
| Sky view | 192 × 108 | the sun moves past a 0.25° threshold |
| Environment cube | 6 × 64² / 128² / 256² (`quality.environment` Low/Medium/High), mips down to a 4² irradiance tail | the sky or the tier changes |
| Cloud shape / detail volumes | 64³ / 32³ (`quality.clouds` Low), 128³ / 32³ (Medium and up) | once per device cloud tier; a tier change bakes the new pair behind the bound one, then retires it |

Diffuse irradiance lives in the environment cube's cosine-convolved tail level; there is no CPU spherical-harmonic projection.

`AtmosphereSettings` exposes Rayleigh, Mie and ozone coefficients, scale heights, planet radius and shell height, observer altitude, ground albedo, solar/lunar disks, star luminance, a final sky calibration, and aerial-perspective controls. Those coefficients define one optical column that the sky's in-scatter, the visible sun and moon disks, and the linked directional lights all share (`hi.atmosphere.transmittance`). The path is a ray-sphere march against `planet_radius` and `atmosphere_height` through the real density profile, so twilight, horizon reddening, and Earth shadow fall out of the geometry rather than a tuned ramp. Aerial perspective: a distance scale of 1.0 treats one world unit as one metre (compact scenes may raise it); strength is a bounded optical-depth multiplier, and zero on either removes the camera-to-surface effect while keeping the sky. Keep Earth defaults unless deliberately authoring another atmosphere. Fog `density_scale` is the weather lever.

## Clouds

`cloud_coverage` is the layer's switch and main dial. At zero (the default) nothing is allocated or scheduled. The layer is a spherical shell around the same planet as the atmosphere tables and is marched into the same environment cube, so ambient, roughness-filtered specular, and every ray-traced miss inherit an overcast sky without knowing clouds exist. The field is Perlin-Worley shape eroded by Worley detail, baked into periodic tiling volumes at startup (no cooked asset, no seam). Wind moves it continuously; the cube is rebuilt once the field has travelled a sixty-fourth of one shape period.

Clouds reach the frame twice from one march function: the cube march converges (full steps, no jitter, no history, anchored at the origin), and the screen march is a reduced-resolution pass anchored at the camera that stops on the depth buffer and is composited by the opaque lighting pass. The screen march has **no temporal history**, only spatial dither, so a still frame renders bit-identically; reprojection can be added later if measurement demands it.

`quality.clouds` is the cost tier, orthogonal to whether a scene has clouds at
all. It expands to a screen resolution scale and two march budgets rather than
gating a feature; `off` spends nothing anywhere, including in the cube, so a
scene with coverage authored lights as clear sky on hardware that cannot afford
the layer. It is authored in **Project Settings → Quality → Cloud
quality** (`render.json`, `quality.clouds`), not on the scene: the scene decides
whether there are clouds, the project decides what drawing them may cost.
