MIT License NuGet

Home / Docs / Lighting and sky

Lighting and sky

Lighting is a list you add to and a sky you configure. Both are assembled once per frame into a single buffer that every pass reads, so shadows, ambient occlusion, global illumination and the sky itself cannot disagree about where the sun is.

Adding a light

BaseApp.Lighting is a SceneLighting instance: a list of LightSource objects plus a constant ambient term. Add lights when objects appear and remove them when objects leave — there is no per-frame rebuild to do.

Lighting.Ambient = new Vector4(0.12f, 0.13f, 0.16f, 1f);

Lighting.Add(new LightSource
{
    Kind = LightKind.Spot,
    Name = "porch",
    Position = new Vector3(24f, 6f, 7.5f),
    Direction = Vector3.Normalize(new Vector3(0f, -1f, -0.4f)),
    Color = new Vector4(1f, 0.86f, 0.66f, 1f),
    Intensity = 40f,
    Range = 24f,
    InnerConeAngle = 0.25f,      // radians
    OuterConeAngle = 0.55f,
    CastShadows = true,
    Priority = 10
});

Kind is Directional, Point or Spot. IsOpen defaults to true, so switching a light off keeps it in the list and out of the bake — cheaper and less error-prone than removing and re-adding it.

Eight lights, and the ninth is simply absent

SceneLightParams.MaxLights is 8 and that is a hard GPU limit: the uniform buffer has eight slots. When a scene has more, the bake sorts by Priority and takes the first eight, breaking ties by distance to the camera. Nothing warns, throws or dims. If a light matters, give it a high priority.

Only one punctual light gets a shadow map per frame, whatever CastShadows says on the rest. A glTF file carrying its own KHR_lights_punctual lights appends them itself, so an imported interior can consume the whole budget — see Models and animation.

Shadows you will actually touch

Three sun cascades and one punctual slot share one square depth atlas. There are fourteen shadow knobs on RenderQuality; in practice you adjust two.

var q = RenderQuality.Current;

q.ShadowDistance = 220f;      // how far the sun casts; default 40
q.ShadowSoftnessTexels = 2f;  // Vogel-disk radius, in shadow texels
Resolution follows distance, not the other way round

Raising ShadowDistance spreads the same atlas over a larger world, so shadow texels grow and contact points soften. If shadows go mushy after you extend the distance, raise ShadowAtlasSize — but that one is an initialization value, so it belongs in the app constructor, not in a settings screen slider.

Acne and peter-panning are handled by ShadowNormalOffset (default 1.5 texels) rather than by pushing depth away from the light, which is why the depth biases can stay small. The full table lives in Lighting and shadows.

The sky is a light source

With the procedural sky on, the sun and moon directions come from the atmosphere model rather than from a light you place. Atmosphere is a static class of physical parameters; the ones worth knowing first are the celestial bodies.

MemberDefaultMeaning
SunDirection, MoonDirectionUp, downDriven by the day-night cycle when one is running.
SunIrradiance12Sun strength reaching the ground.
MoonIrradiance0.6Moon strength. Deliberately not zero, so night is navigable.
NightAirglow0.004Faint emission that keeps a moonless night from being pure black.
StarRadiance0.15Star brightness; StarRotation and StarPoleAxis orient the field.
CloudsSkyState.ClearCloud layer set. See below.
ViewAltitudeKm0.2Where the camera sits in the atmosphere, which changes the horizon.

Rayleigh and Mie coefficients, ground and atmosphere radii and ozone parameters are all there too, set to Earth values. They are the reason a sunset looks like a sunset without any authored gradient, and they are also the reason you should change them one at a time.

Weather, as an interpolation

Clouds are a SkyState: a set of layers with coverage, altitude, thickness and albedo. Four presets exist, and the useful move is not picking one but blending between two.

// A preset.
Atmosphere.Clouds = SkyState.Overcast;

// Or a weather curve: t moves the sky continuously from fair to stormy.
Atmosphere.Clouds = SkyState.Lerp(SkyState.Fair, SkyState.Storm, t);

The default is SkyState.Clear, which has zero layers, so an application that never mentions clouds pays nothing for them. Wind offsets per layer live in Atmosphere.CloudWindOffsetKm, which is how a layer drifts without the noise field being regenerated.

Time of day

WorldSettings owns the clock and DayNightCycle does the conversions. Speed and start hour follow the same two-faced shape as render quality: static Default* fields for the app constructor, instance properties that round-trip through settings, and Current as the runtime read.

// In the app constructor. At the default speed of 0.01 a day takes 100 seconds.
WorldSettings.DefaultDayNightSpeed = 0.004f;
WorldSettings.DefaultStartHour = 5.5f;

// Anywhere afterwards:
float phase = DayNightCycle.PhaseFromHour(18f);   // dusk
float hour  = DayNightCycle.HourFromPhase(phase);

The model is an equinox simplification with a twelve-hour day, so SunriseHour is 6, noon lands on 12 and midnight on 0 with no correction. The moon runs on its own SynodicDays cycle of 29.53, so it waxes and wanes across in-game months rather than repeating daily.

What to read next

Render quality covers the rest of the knobs, including the distinction between values read once at startup and values you can write every frame — which is the single most common early mistake with this API.

For the reasoning behind the pipeline itself, see Lighting and shadows, Sky, atmosphere and weather and Global illumination and AO.