Home / Docs / 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.
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.
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.
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
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.
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.
| Member | Default | Meaning |
|---|---|---|
SunDirection, MoonDirection | Up, down | Driven by the day-night cycle when one is running. |
SunIrradiance | 12 | Sun strength reaching the ground. |
MoonIrradiance | 0.6 | Moon strength. Deliberately not zero, so night is navigable. |
NightAirglow | 0.004 | Faint emission that keeps a moonless night from being pure black. |
StarRadiance | 0.15 | Star brightness; StarRotation and StarPoleAxis orient the field. |
Clouds | SkyState.Clear | Cloud layer set. See below. |
ViewAltitudeKm | 0.2 | Where 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.
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.
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.
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.