voxolith

5. Day and night

Rat Hunt, step 5: a game clock that drives timeOfDay and atmosphereFrame from morning to dusk, and point lights that come on as it gets dark.

The game lasts one day. In this step a clock moves the sun from mid-morning to after sunset, the sky and the light change with it, and as it gets dark a lamp by the door and a lantern you carry come on.

The clock

timeOfDay(phase) turns a single number into every lighting field of a frame: the sun's direction and colour, ambient light, the sky gradient, the moon. The phase runs over a day: 0 is midnight, 0.25 sunrise, 0.5 noon, 0.75 sunset. Rat Hunt runs from 0.3 to 0.78, early morning to just after sunset, over three minutes:

import type { PointLight, Vec3 } from "@voxolith/renderer";

const DAY_SECONDS = Math.max(10, Number(params.get("day") ?? 180));
const DAWN = 0.3, DUSK = 0.78;
let clock = 0;

?day=30 on the URL makes a 30-second day, which is handy while you test the evening.

Remove the fixed sky constant from step 2; the frame now computes it from the clock:

    clock += dt;
    const phase = DAWN + (Math.min(clock, DAY_SECONDS) / DAY_SECONDS) * (DUSK - DAWN);
    const lighting = timeOfDay(phase);
    renderer.setLights(lights(lighting.nightFactor));
    status.textContent = `${Math.max(0, Math.ceil(DAY_SECONDS - clock))} s to nightfall`;

and renders with it:

    renderer.render({ ...view(), ...atmosphereFrame(lighting, ATMOSPHERES.clear), time: now / 1000 });

atmosphereFrame combines the lighting with an atmosphere. ATMOSPHERES.clear changes nothing; try ATMOSPHERES.cloudy, rain or fog to see the same day under other weather. The engine only describes and blends weather: when it changes, and how wet the ground gets, is the game's decision (see Atmosphere and weather).

At the top of the file, next to canvas, take the HUD's status line:

const status = document.getElementById("status")!;

Lights

The sun and moon are one directional key light, which timeOfDay controls. Anything else that gives light is a point light, a position with a colour and a hard range beyond which it adds exactly nothing. Up to 32 at a time are passed with renderer.setLights.

const doorLamp: Vec3 = [hx, pad + 20, hz1 + 3];

function lights(night: number): PointLight[] {
  if (night < 0.02) return [];
  return [
    { position: doorLamp, color: [1, 0.72, 0.4], intensity: 2.4 * night, range: 90, glow: 2 * night },
    // A lantern the player carries: no shadow ray, so it costs little.
    { position: [player.x, player.eyeY - 4, player.z], color: [1, 0.85, 0.6], intensity: 0.9 * night, range: 60, shadows: false },
  ];
}

nightFactor from timeOfDay goes from 0 by day to 1 at night, so both lights fade in through the evening instead of switching on. By day there are no point lights at all, which costs nothing.

  • The door lamp stands just in front of the door, 20 voxels up, at the top of the frame. Its glow draws a visible halo, so the lamp itself shows.
  • The lantern follows the player, a little below the eye. It sets shadows: false: every shadowed light traces an extra ray per pixel it reaches, and a light at the camera casts shadows you would hardly see.

The lit windows from step 4 are emissive: they glow, but light only their own voxels. To make a window light the ground outside, put a point light at it.

Run it

Load the page with ?day=30. The sun climbs, swings across and reddens; the shadows lengthen; at dusk the lamp and the lantern come on and the windows glow. The HUD counts down to nightfall. When the clock runs out the evening simply stays; the next step makes that the end of the game.

See Lighting for shadow cost and the quality presets' shadowSteps.

Next: rats.

On this page