Sun position, time of day and directional lighting in pixel-art games, across perspectives

Scope: how a 2D pixel engine (Tycho: per frame only fillRect, drawImage, alpha, 4 blends, transforms, gradients, tint, lightMap, drawPixels; everything heavier pre-baked offscreen) can move a sun across the sky and keep the lighting correct in top-down, 3/4 oblique, isometric 2:1, side view, military or cavalier oblique, and pseudo-3D views.

Related notes in this folder, not repeated here: effects_rendering.md (light grids, Tycho.light.Smooth, normal-map generator paper, cloud shadows, crossfading two reprints for day and night) and local_games_and_tycho.md (the stand-in’s tint/lightMap, Lodestar’s stepped light, White Dragon’s lightMap).

Research note: shipped games rarely publish their time-of-day internals. The most complete primary source found was Lazy Bear’s Graveyard Keeper write-up. Sea of Stars, Eastward, Kingdom, Terraria, Owlboy, Celeste, Unrest, Hyper Light Drifter, Moonlighter and Witchbrook have little or no published technical detail. The projection math in the Inferences sections is derived here from standard parallel-projection geometry and is labeled as derivation, not as a sourced claim.

How shipped games handle the time of day

Takeaway

Two families exist. Full 3D lighting systems light real or proxy geometry: Sea of Stars’ custom pipeline, Eastward’s 3D-with-bump-maps engine, Octopath’s UE4 with invisible shadow proxies, Graveyard Keeper 2’s projected pixel art on 3D models. Cheap 2D systems are a global ambient color or color-grade LUT keyed to the clock (Stardew, Don’t Starve, Graveyard Keeper 1’s 10 LUTs), plus a few rotating shadow sprites per object. Only the second family fits Tycho’s stand-in directly. Graveyard Keeper 1 shows it can still look “dynamic”.

Cited Findings

Graveyard Keeper (Lazy Bear Games, Unity): the most detailed primary source

Sea of Stars (Sabotage Studio)

Eastward (Pixpil, custom engine descended from MOAI)

Octopath Traveler (HD-2D, UE4)

Stardew Valley

Don’t Starve / Don’t Starve Together (Klei)

Terraria

Children of Morta (Dead Mage, Unity)

Witchbrook (Chucklefish)

Others found

Inferences

Gaps

Cast shadows from a sun direction in each projection

Takeaway

A shadow’s length is height·cot(elevation) and its direction is opposite the sun’s azimuth. In every parallel projection (top-down, 3/4, iso, dimetric, military, cavalier) the shadow of an upright sprite card is an affine image of the sprite: one setTransform and a drawImage in a flat shadow ink. That is exactly what Tycho’s stand-in can do per frame. Height-field ray marching gives correct self-shadowing on terrain but needs a shader or a pre-bake. Shadow volumes and shadow maps belong to the pseudo-3D tier.

Cited Findings

Inferences (derivations, not sourced)

General formula (all parallel projections). World axes: E (east), N (north), Z (up). The sun has azimuth A (clockwise from north) and elevation e. The vector toward the sun is s = (sinA·cos e, cosA·cos e, sin e). A point at height h lands on the ground at offset d = −h·cot(e)·(sinA, cosA). Shadow length is L = h·cot e, pointing toward azimuth A+180°.

Let a projection map the world unit vectors to screen vectors ê, n̂, ẑ (ẑ points up the screen, so it has a negative screen y). Then a sprite card drawn with its foot at F, horizontal axis û (usually (1,0)) and pixel height v (v screen px up = v/ ẑ world units) has a shadow that is the affine map
(u, v) → F + u·û + v·k·(−cot e)·(sinA·ê + cosA·n̂), with k = 1/ ẑ .

For canvas this is setTransform(û.x, û.y, c.x, c.y, F.x, F.y) with c = −k·cot e·(sinA·ê + cosA·n̂), applied to the sprite with its rows flipped so v counts up from the foot. One matrix covers every sun position: a sun behind the object gives a shadow up-screen behind it, a sun in front gives a flipped shadow toward the viewer, a sun to the side gives a pure shear.

Per view (screen y down):

Pixel-art discipline.

Gaps

Shading the faces of sprites and tiles by sun direction

Takeaway

Lambert is linear: I = ambient + diffuse·max(0, n·s). So a sprite lit from a few fixed directions can be blended, and flat-faced iso boxes need only one dot product per face, then a ramp lookup. Keep it pixel art by quantizing to the material’s 3–4 tones (cel or banded lighting) and re-printing offscreen per sun step. Per-pixel normal maps need a shader or N pre-baked directions.

Cited Findings

Inferences

Gaps

Sky and ambient color through the day

Takeaway

Games don’t run physical sky models for pixel art. They key a small table (4–10 phases: night, blue hour, golden hour, day, plus moon) on time or, better, on sun elevation. Each phase holds sky gradient stops, an ambient multiply color, shadow ink and alpha, and either a color-grade LUT (Graveyard Keeper’s 10, Don’t Starve’s day/dusk/night/full_moon cubes) or, for a true indexed palette, a palette remap row (“U is color, V is time”). Coherent nights come from hand-designed night palettes, not from darkening day colors.

Cited Findings

Inferences

Gaps

Performance tricks

Takeaway

Everything sun-dependent changes slowly, so bake it in steps. Quantize the sun to K directions, re-bake shadows, face shading and rims offscreen only when the step changes (or spread the work over several frames), and crossfade or hold between steps. Per frame, draw a few affine drawImage shadows, one ambient tint or lightMap, and a sky gradient. That is all stand-in-safe.

Cited Findings

Inferences

Gaps

The sun’s path itself: a solar position model good enough for games

Takeaway

Three equations are enough: declination from day of year (a cosine fit, about 2° error), hour angle at 15° per hour from solar noon, then elevation by sin α = sinΦ·sinδ + cosΦ·cosδ·cos h and azimuth by an atan2 form. Sunrise and sunset come from cos h₀ = −tanΦ·tanδ, which gives season-dependent day length for free. Games can also simply fake an arc, but this model costs only a few trig calls per sun step.

Cited Findings

Inferences

Gaps