Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsLicenseBack to top
Documentation / Guides
Browse docs
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Guides10 min read

Time of day

On this page
On this pageRuntime controllerSunset is extinction, not a colour rampScene authoring and editor previewProduction choreographyEnvironment ownershipHow the sky is builtClouds Back to top

Hikari treats a dynamic sky as one environment source, not as a sequence of skybox swaps. AtmosphereSettings drives the visible clear sky, diffuse SH, 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 SH and specular IBL, so it does not double-light surfaces. A shadowless moon directional is optional; dynamic atmosphere SH 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.

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: src/games/bistro/src/entities/time_of_day_entity.zig.

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(),
    sun: hi.ActorRef,
    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(sun, 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.
  • Reflection probes remain the local-interior override. The global atmosphere remains the fallback outside probe influence.
  • 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.

ResourceTexelsRebuilt when
Transmittance256 × 64the medium changes (profile edit)
Multiple scattering32 × 32the medium changes
Sky view192 × 108the sun moves past a 0.25° threshold
Environment cube6 × 128² + mipsthe sky changes
Cloud shape / detail volumes128³ / 32³once per session

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.

PreviousShader authoringNext User interface

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/guides/time-of-day.md
On this pageRuntime controllerSunset is extinction, not a colour rampScene authoring and editor previewProduction choreographyEnvironment ownershipHow the sky is builtClouds Back to top