Skip to content

Why Your Procedural Planet's Night Side Is Still Lit

21 July 2026 · Devlog

When I panned my procedural UE5 planet across the terminator, the night side was glowing: white clouds floating over black terrain, with gold flecks scattered across the dark ground. This is the debugging story behind that. It turned out to be two bugs, one red herring and a shader-space day/night terminator. Below I cover why a planet breaks the lighting assumptions every other scene relies on, the one cloud checkbox that fixes glowing night-side clouds, why the sun shines through the planet and shadows can’t fix it, a cheap per-pixel terminator driven from a Material Parameter Collection, and the exposure setting nobody tells you about.

A Planet Shows Day and Night at the Same Time

Every normal scene is local. You stand inside it, and the sun is either up or down. From orbit you see the whole globe: the lit hemisphere, the dark hemisphere and the terminator between them, all in one frame. Every lighting shortcut that assumes “one place, one sun state” now renders both states at once, and if the dark side isn’t actually dark, the illusion collapses instantly.

Here’s what “wrong” looked like in practice:

  • Clouds: bright white on the night side, same as noon.
  • Terrain and foliage: a gold speckle crawling across sun-facing slopes, densest near the terminator and fading into deep night.
  • The whole frame: washing brighter the moment I looked at the dark side.

Three different symptoms, and as it turned out, three different causes.

Diagnosis: One Red Herring, Two Bugs

It wasn’t the Sky Light

The obvious suspect was Sky Light ambient. It’s global, so surely it was filling the night side. I switched the Sky Light over to a scene capture, then turned it fully off, and the clouds were still white. Ambient wasn’t lighting them.

When the obvious fix changes nothing, you’ve got the wrong model, and probably more than one bug wearing the same coat. Kill one variable at a time: “disable it entirely” is the fastest diagnostic you own.

Separating the clouds from the ground

So I toggled the two lights independently:

  • Directional sun to zero: the gold ground speckle vanishes, but the clouds stay lit.
  • Sky light to zero: nothing changes on the clouds.

The clouds are self-lighting through the cloud system, while the terrain is lit directly by the sun. Two independent problems, so I fixed them separately.

Clouds: The Single-Transmittance Trap

UE’s Volumetric Cloud can evaluate the sun’s atmospheric transmittance once for the entire layer. For a local sky, that’s a big optimisation. On a planet, that single value is sampled where the layer is lit and then applied to the clouds on the night side too. The dark-hemisphere clouds are effectively told “the sun reaches you fully”, so they render white everywhere. It isn’t ambient and it isn’t emissive. It’s one shared number stretched across the terminator.

The fix is one checkbox: enable Use Per Sample Atmospheric Light Transmittance on the Volumetric Cloud component.

Cloud->bUsePerSampleAtmosphericLightTransmittance = true;
Cloud->MarkRenderStateDirty();

With this on, every cloud sample computes its own transmittance. Night-side samples get roughly zero, so they go dark, and the white band at the terminator becomes a clean day-to-night falloff. We set it automatically when the LOD system configures a planet’s atmosphere, so users never have to hunt for it. This is the night-cloud fix.

Terrain: The Sun Shines Through the Planet

A directional light isn’t blocked by anything

A directional light is infinite parallel rays. It lights every surface whose normal faces it, wherever that surface is. The smooth sphere is handled by N·L, so the macro night side does go dark, but the terrain has detail. Steep slopes and foliage cards on the night side that happen to tilt toward the sun get full gold sunlight, because nothing occludes them. That’s the speckle: the sun is literally lighting geometry on the far side of the planet.

Shadows can’t save you here

The reflex answer is “just turn on shadows”. But cascaded shadow maps cover metres to hundreds of metres around the camera, and the planet is kilometres across. Against a 50 km planet, the sun’s shadow distance is a rounding error. Distance-field shadows don’t help either, because runtime procedural meshes have no distance fields to trace. Self-shadowing the whole globe per pixel isn’t on the table, so I stopped thinking about shadows.

Darken in shader space instead

We don’t need a shadow. We need the night hemisphere to stop responding to light.

Every point already knows which hemisphere it’s on: compare its outward direction to the sun direction. The important detail is to use the macro sphere normal (the direction from the planet centre), not the pixel normal. That way a sun-facing slope on the night side is still treated as night. That single dot product is a per-pixel, shadow-map-free day/night mask.

The fix isn’t more shadow resolution. It’s asking “which side of the planet am I on?” in the material.

Driving the Terminator From the Material

Publishing the planet centre and sun direction

The materials need two world-space vectors: where the planet is, and where the sun is. The LOD subsystem writes both into a Material Parameter Collection for the nearest planet, every time they change:

Center = Planet->GetActorLocation();
SunDir = -Sun->GetForwardVector(); // direction TOWARD the sun (light shines along +Forward)
MPCI->SetVectorParameterValue("PlanetCenter", Center);
MPCI->SetVectorParameterValue("SunDirection", SunDir);

That one collection is sampled by every terrain and foliage material (terrain, grass and trees alike), so there’s no per-material wiring of data.

Dot, smoothstep, multiply

In the material graph, the terminator comes down to this:

up = normalize(AbsoluteWorldPosition - PlanetCenter); // macro sphere "up"
ndl = dot(up, SunDirection); // +1 noon · 0 terminator · -1 midnight
day = smoothstep(-Softness, +Softness, ndl); // soft dusk band, ~0.1–0.2
BaseColor *= day; // fade albedo to black on the night side
Specular *= day; // AND kill the specular glints — this is what removes the gold speckle

Multiply both BaseColor and Specular. Darkening diffuse alone leaves the specular sparkle behind. Wrap the whole thing in a Material Function and it becomes one node you can drop into any material.

Two ways to get it backwards

  • Subtraction order: WorldPosition − PlanetCenter, not the reverse. The outward normal points from the centre to the surface; flip it and you light the night side instead.
  • Origin: use the planet centre, not ObjectPosition. Streamed terrain chunks each have their own pivot, so ObjectPosition seams at every chunk border.

Taking the centre from the MPC also means planets can sit anywhere in the level, not just at the world origin. It’s the same radial-up gotcha as the slope mask: on a sphere, “up” is a parameter, never a constant.

Don’t Write the Collection Every Frame

A static sun should cost one write, ever. The naive version pushes the MPC every tick, re-dirtying the collection’s render resource for values that never change. Instead, compare against the collection’s current value first and only write on a real change:

if (!Cur.Equals(NewVal, Tol)) MPCI->SetVectorParameterValue(Name, NewVal);
  • Static sun: written once and never again.
  • Day/night cycle: gated by a coarse ~0.1° threshold, because the terminator band is soft enough that sub-degree steps are invisible.

The result is a handful of writes per second even mid-rotation, scaling with how fast the sun actually moves. “It’s cheap” isn’t “it’s free”, but once the update is gated on change it’s genuinely free when nothing moves.

The Exposure Nobody Mentions

With the clouds and terrain fixed, I looked at the night hemisphere and the whole frame brightened. Auto-exposure (eye adaptation) sees the dark hemisphere in frame and cranks the gain, washing out the day side and re-lifting everything I’d just darkened.

The fix is a fixed exposure in a Post Process Volume for the planet-from-space view. I’m using roughly EV 10 here. Instead of the milky, flat look auto-exposure gives, you keep deep blacks on the night side. The plugin doesn’t force this, since it’s your scene’s call, but the demo ships with it set.

Many Planets, One Collection

The MPC carries one PlanetCenter, and the LOD system points it at the nearest planet: the one you’re actually on or near. That’s correct for the streamed LOD0 world and for the planet filling your view.

Distant LOD1 planets are dots. If you still want each of them to have its own terminator, feed Object Position into the far-sphere material instead of the MPC centre. A single-mesh sphere’s pivot is its centre.

The Result

Orbiting from full day into full night, the clouds fade to dark at the line, the ground drops to black, the gold speckle is gone and the exposure holds. Pull back to the whole globe and you get one clean lit half and one clean dark half. One cloud checkbox, one dot product, one exposure setting.

Honest Tradeoffs

  • The terminator is a macro latitude fade, not real self-shadowing. Mountains don’t cast long dusk shadows across their own valleys.
  • The night side goes to near-black. There’s no moonlight or city lights unless you add them; a NightFloor lifts it slightly.
  • It’s per-material. Every terrain and foliage material has to sample the terminator, though a Material Function keeps that to one node.
  • With multiple planets, only the nearest planet’s terminator is exact from the shared collection.

What It Actually Costs

  • Clouds: per-sample transmittance is a little more work inside a raymarch you’re already paying for. It’s correctness, not a new pass.
  • Terminator: a subtract, normalize, dot, smoothstep and two multiplies per pixel, which is noise-floor cost.
  • Data: an MPC update only when the sun or the active planet changes, so zero per-frame cost for a static sun.

No shadow maps, no captures, no extra render passes bolted on. The whole fix is a checkbox, a few ALU ops and an occasional vector write.

The Transferable Pattern: Which Side of the Sphere Am I On?

Any planet-scale surface, whether that’s oceans, ice, city-light emissive or atmosphere rims, can gate on the same dot(up, sunDir).

The structural lesson: when the engine’s occlusion can’t reach your scale, answer the lighting question analytically in the material instead of forcing a shadow system to do it. And publish world facts such as centre, sun and time through a parameter collection, so every material shares one source of truth.

You don’t always need a shadow. Sometimes you just need the angle.

Where to Get It

This ships in PlanetGen V1.6, available on Fab for UE 5.4–5.8. The playable packaged demo is on Patreon if you want to fly across the terminator yourself. The plugin includes full C++ source: the cloud flag and terminator driver live in PlanetGenLODSubsystem.cpp, and the material side is a drop-in function.

In the next video I’ll cover the far-halo shells that give distant LOD1 planets an atmosphere on the cheap.