All work

Personal project

Time of Day: Runtime Day/Night Lighting System

The scene shipped lit exactly one way: baked, static, night. Making it a second time of day was not a slider — it was a second bake. So I removed the bake entirely and replaced it with a system.

Ownership: Personal project, worked on alone. The environment is “Dreamscape: Stylized Environment Tower” by Polyart Studio (Fab), used as-is — the models, materials and level layout are theirs, not mine, and this is not presented as my environment work. What is mine is the conversion from baked to dynamic lighting, the C++ Time of Day system, both lighting presets, and the optimisation described below.

The cave entrance lit by the Day preset: warm sunlight raking across pale rock, a tree and flowering grass in the foreground, and the circular latticed door in cool shadow.
My role
Technical artist — lighting pipeline, C++ tooling, optimisation
Ownership
Personal project
Status
Complete
Tools
Unreal Engine 5.4, C++, Movie Render Queue

What I did

  • Converted a baked level to fully dynamic Lumen (DX12/SM6, Virtual Shadow Maps)
  • Wrote the C++ Time of Day controller and its preset struct
  • Authored and tuned the Day and Night lighting presets
  • Cut dynamic shadow casters from 143 to 11

Assets: The environment is “Dreamscape: Stylized Environment Tower” by Polyart Studio (Fab), used as-is — models, materials and level layout are theirs. The lighting conversion, the C++ system, both presets and the optimisation are mine.

Measured

Dynamic shadow casters
143 → 11Counted in-editor across all light components before and after the candle-Blueprint shadow fix; recorded in the project handover doc.
Baked lighting data removed
1.1 GBSize of DemoMap_BuiltData.uasset, moved out of the content directory.
Lights converted to Movable
49 standalone + 264 in BlueprintsEnumerated in-editor: 34 Point, 12 Rect, 3 Spot, plus 2 per candle across 132 instances.

What was actually in the box

The level arrived fully baked: 1.1 GB of lightmap data, a Static directional light at 1 lux, and a Post Process Volume that explicitly forced Lumen global illumination and reflections to None. The project was also running DX11 with Shader Model 5, so Lumen and Virtual Shadow Maps were not merely disabled — they were unavailable.

That rules out the obvious approach. You cannot brighten a baked night into a day, because every bounce you can see is stored in a lightmap that was solved for one sun position. A day version means a second bake, and a second bake means every subsequent tweak costs hours.

Removing the bake

I moved the project to DX12 / SM6 and enabled Virtual Shadow Maps, then set the sun, the sky light and all 49 local lights to Movable, with the sky light on Real-Time Capture. The 1.1 GB of built lighting data had to leave the content directory entirely: with the sun Movable it no longer contributes, but Unreal will still apply the stale baked night on top of Lumen if the file is present.

I chose software Lumen over hardware ray tracing. Switching to DX12 would have silently activated the project’s dormant ray-tracing flags across every level, and the target here was a playable frame budget on a mid-range GPU, not a maximum-quality still.

The important constraint was blast radius. This project contains around thirty other levels that are still baked, so Lumen is enabled only on this one, through its own Post Process Volume overrides, rather than by flipping the project-wide default. Nothing else in the project changed appearance.

The system

The controller is a C++ actor holding two instances of one struct. A preset describes an entire look: sun angle, colour, intensity and volumetric scattering; sky intensity and colour; height fog and volumetric fog; the post-process grade; multipliers for the level’s local lamps; and which particle systems are visible.

Apply Day and Apply Night are exposed as editor buttons, so the viewport updates live while you tune, and the same code path runs at runtime. Because a look is data rather than a saved level state, blending between the two is a lerp across the struct.

One design detail mattered more than it looks. The local lamp multipliers scale relative to a saved record of what is already applied, not to a sampled baseline. The first version sampled live intensities on load — which meant every reload treated the already-scaled values as the new baseline, and the multiplier compounded silently. The lamps had reached 2.7× their authored brightness before I caught it.

Making it cheap enough to run

The pack’s candle Blueprint carries a point light with dynamic shadows enabled. That is invisible in a baked scene and ruinous in a dynamic one: 132 candle instances meant 132 shadow-casting lights, each of them a 10-lumen source with a 200 cm radius — the kind of light whose shadows nobody will ever see.

Turning shadows off on that one component took the level from 143 dynamic shadow casters to 11. I applied it as a per-instance override rather than editing the shared Blueprint, because other levels in the project use the same asset and none of them asked for a lighting change.

Three bugs worth writing down

Renders came out white while the viewport looked correct. The editor viewport applies physical camera exposure — f/4, 1/60 s, ISO 100, about 9.9 EV of it — and Movie Render Queue does not. Every value I had tuned by eye in the viewport was roughly ten stops too bright when rendered. Pinning that setting off in both paths and re-baselining the exposure bias closed the gap. I was wrong twice before I found it.

Volumetric fog effectively only exists at Cinematic scalability. Movie Render Queue forces Cinematic through its Game Override settings, so fog I had tuned in a default viewport — where volumetric fog is quietly off — rendered as an opaque white-out. It was around seventy-five times too dense. Fog tuning now happens with the viewport pinned to Cinematic, which is the only way what you see matches what renders.

The blend flashed magenta at its midpoint. Interpolating light colour with Unreal’s HSV lerp takes the long way round the hue wheel between a warm sun and a cool moon, and passes straight through magenta on the way. Linear RGB fixed it, and the midpoint now reads as a believable dusk.

Breakdown

Where the eye goes

What the night preset is built around. The brightest, highest-contrast point in the frame is also the narrative focal point, which is why the door needs no signposting — and why the lamp multipliers rise at night rather than the sun.

Dual lighting as the eye pathA wide cinematic frame in which cool ambient shadow is punctured by a warm, high-contrast pool of lamplight at the circular door. The eye travels from the foreground grass along the rock pathing to that focal point, while the palette desaturates with distance.Wide-angle cinematic framingCircular doorfocal pointForeground grassCool ambient — moonlight on rockWarm, high contrast — door lampsForeground palette vibrant → desaturates with distanceRock pathing and the central tree carry the eye from the grass to the rock face

Demonstration

Warm key, cool fill

Two opposed lights orbiting a single form. The warm key reads as the door lamps, the cool fill as moonlight — the split the night preset is built around, and the one the day preset inverts.

Shown as a live 3D view on larger screens with motion enabled.

Gallery

View on ArtStation

Next

More breakdowns

  • Horizontal bar chart comparing GPU milliseconds per render pass before and after optimisation, measured at 2560 by 1440 on an RTX 3060. TemporalSuperResolution falls from 5.83 to 3.70 milliseconds, RenderDeferredLighting from 3.57 to 0.96, RenderVelocities from 2.65 to 0.05 and ShadowDepths from 3.70 to 2.63, while ShadowProjection rises from 2.61 to 3.09 and Prepass from 0.19 to 2.01.

    Performance Audit

    A measured optimisation pass on a scene I did not author: 30.45 → 22.60 ms GPU frame time at native 1440p, with the method and the missed target reported in full.

  • Unreal Engine 5 material instance editor showing the layered material stack with base wood, painted metal and emissive trim layers.

    Modular Layered Material System

    An Unreal Engine 5 master material driven by ID masks, so one mesh can swap between wood, painted metal and emissive trim without leaving the engine.

  • Wide high-angle view of a sci-fi maintenance hangar built from a modular kit, captured in the Unreal Engine viewport, with a central lit maintenance platform, overhead gantries and yellow crane supports.

    Maintenance Hangar

    A large-scale sci-fi hangar built from a custom modular kit, lit with Lumen and textured almost entirely from a single trim sheet.