itch.io is community of indie game creators and players

Devlogs

Atmosphere: Lighting, Shadows and Optimisation

Recursion
A downloadable game

Dev Blog: Lighting, Post-Processing, Shadows & Reflections


Introduction 

Lighting in Recursion has always been one of the strongest contributors to atmosphere. It first emerged as early as the greyboxing stage, when I began experimenting with room lighting controls. From this early development stage, lighting and atmospheric effects became a key environmental mechanic. The visual goal was for a sterile, liminal and brutalist theme.

During the earliest passes of the lighting mechanic, point lights were initially used, which wasn’t a problem at the time – but as the project evolved and grew, the sheer number of lights became a massive performance hit. This blog post will document the process of development with lighting.


Early Research

After doing some initial research into whether to pursue real-time lighting or baked lightmaps, I decided real-time was the way to go. Seeing rooms in different states of lighting gave each space a few different ambient variations. It also hinted at the world being dynamic which is something I wanted to home in on and really push.

By using baked lightmaps, each space was always lit and clashed with its real-time counterpart. Blocky artifacts from the lightmap were visible and made for an unpleasant aesthetic. Furthermore, with the goal of having props moving meant that shadows didn’t update correctly when objects moved or disappeared, which whilst optimized and looked somewhat nice, but wasn’t the right feel. Additionally, blocky lightmaps broke immersion and detracted from the unsettlingly clean, liminal feel I wanted.

Baked Lightmap Artefacts

I briefly explored Unity’s light probes, which yielded better results, but worked mainly for global directional lighting, this approach looked nice but dealing with light bleed from the directional light in interior spaces was consuming too much time, the trade-off between visual aesthetic and practicality made this trade-off not worth it.

Washed out ceiling lighting from directional light bleed

In the end, real-time spotlights were used for lighting the environment, with a select few point lights in each room which cast shadows.


Why Real-Time Lighting?

By using real-time lighting, it gave me the flexibility to control the visual feel of each room dynamically.

My initial approach was naïve, using point lights on every overhead light. It made the space look appealing at first glance, but came with a few major downsides. Reflective surfaces displayed all the light sources, and further research (https://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/#point-lights) uncovered that point lights use a cubemap: allowing for shadows to be cast around the point light in 6 directions. This isn’t bad for 2-3 lights in a room for casting shadows in key locations – but using them for 20-30 lights killed performance.

These point lights were replaced by spotlights for illuminating the walls and floors, whilst 1-2 point lights were placed per room for dramatic shadow effect. This boosted my frames from a sluggish 50-60 FPS back to a very nice 100-120.

This change also created stronger contrast and tension in the rooms, allowing for shadows to be selectively used to much greater, dramatic effect.


Reflection Probes

At this point in development, it still felt like something was missing. The spaces looked nice with lights and shadows, but it still didn’t quite capture the liminal/clinical aesthetic I was going for. To achieve this, I experimented with Reflection probes (https://docs.unity3d.com/6000.2/Documentation/Manual/ReflectionProbes.html).

Before Shadows, Reflections and Redesign
After Shadows, Reflections and Redesign

After experimenting with a few options of updating these each frame, or on awake, it was decided to bake one reflection probe upon awakening and leave it. These were hooked up to my room controller, which turned the reflection probe off whenever lights were turned off, since otherwise they left visual artefacts that made the room look lit, despite having lights turned off. This was key for maintaining the cold, clinical mood I was targeting – reflections needed to feel accurate but not betray the darkness when a room was unlit


Post-Processing

The last thing in the visual toolkit was the post-processing effects. A very slight amount of bloom was used along with screen-space lens flares which really allowed my lights to pop.

Depth of field was experimented with – I found too much made me motion sick, which led to it becoming a dynamic, subtle effect whenever the player was looking at something they could interact with, as a way of intentionally pulling focus to the target subject.


Very subtle colour grading and tone-mapping was also employed to further balance the colour scheme and visual tone I was building towards. The slight bloom added a sterile, fluorescent glow, while tone-mapping pushed the palette toward a colder, oppressive feel.


Performance

Due to the nature of the way my scenes were lit, and how resource intensive real-time lights and shadows were, it was clear that I needed to optimise my scene’s lighting. My frames were hovering at around 60-80 FPS by this stage. As my level lacked a lot of geometry, it was worrying to have this much of a dip in performance this early on. Profiling confirmed that lighting and shadows were consuming far more resources than anticipated.


Optimisations

The first optimisation was turning lights off using my room controller, this was handled in a specific way to prevent visual artefacts from occurring, i.e. point lights illuminating the room whilst the overhead lights turned off. When turning the room’s lights off, point lights were disabled first, followed by the room’s spotlights and finally the reflection probe. The reverse sequence was employed when turning lights back on.

This still wasn’t enough, I needed a way to cull lights. After investigating Unity’s in-built OnBecameVisible and Invisible methods, I did a hacky approach of attaching an invisible quad to my point lights so that they could be reliably culled when outside the camera’s frustum. This approach further boosted frames considerably, giving me a nice CPU/GPU budget to work with for the later polishing stages of development.

These weren’t just performance hacks, they also ensured lighting and shadows only existed where they were relevant, keeping the atmosphere consistent.


Volumetric Lighting

To further aid in adding immersion, I briefly experimented with volumetric lighting using a custom full-screen HLSL shader. This was fun to make and work with for adding light shafts, but it required the use of a directional light which illuminated everything in the scene that used its light layer.

[

For a while I boxed rooms in with planes and cubes to block the light, but this was a cumbersome process. Seeing as the only areas directly affected were windows – and the areas immediately surrounding them, the trade-off between light bleed everywhere and having some nicely lit windowed areas was a tough one.

Instead, I cut that approach and investigated ways to fake these effects using quads/planes/boxes. After some work in shader graph I made some shaders which allowed for light shafts that allowed me to keep my interior lighting scheme in-tact, and limit where I wanted these lighting effects to take place. This gave me control to inject atmosphere only where I wanted it, making certain spaces feel striking and cinematic without washing out the interiors.


Render Layers and Layer Culling

Up until this point, everything rendered on the one light layer, which was fine when the game only used a single level. Issues arose when I used reflective surfaces on the ground floor, as they were illuminated by third-floor lights, which broke immersion.

To combat this, I separated lights into different layers (https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@12.0/manual/lighting/light-layers.html). Layer 1 was used for the ground floor and level 1, and layer 3 for the third floor.

For each object’s mesh renderer, I assigned it to a shadow-casting only layer only where I wanted shadows present.

The result was a separation of concerns for each floor, allowing each floor to feel more self-contained and believable, reinforcing the sense of isolation per level.

After Light Layers



Conclusion

Altogether, this iterative process of refining lights, shadows, reflection and post-processing was less about raw fidelity and more about crafting a specific mood: sterile, liminal, and quietly oppressive.

Download Recursion
Leave a comment