How to add glow and 2D lights to an old 2D Unity URP game

How to give an old 2D Unity game selective glow with URP Bloom and HDR, dynamic 2D lights and a sharp UI for current screens, using the game Dreamica.

Three shots from Dreamica: a ghost chasing a bubble, a squirrel with a cannon and glowing lasers in a forest

An old 2D Unity game on the built-in render pipeline looks flat on a current phone: there is no lighting, glow is either missing or lights up the whole frame including the UI, and the interface sprites are blurry. In Unity URP you fix this with three things: Bloom with a threshold of 1 plus HDR colour only on objects that should glow, 2D lights from the 2D renderer, and a UI with larger sprites that respects the screen's safe area.

We show the process on our game Dreamica (App Store, Google Play). It is a falling adventure where you draw lines with your finger to guide a bubble with a sleeping child through four worlds. It was originally built in Unity 5.3, with the UI in the NGUI plugin, glow from the MK Glow plugin and art made for the iPhone 7 and its 1334 × 750 display. The gameplay stayed the same; only the graphics changed.

You often reopen an old game because of the store, not the graphics. Since August 2019 Google Play requires 64-bit versions, and a game with only a 32-bit build drops off it. That is exactly what happened to Dreamica. Once you are moving the project to Unity 6 anyway, the graphics are a relatively cheap bonus.

Why the old full-screen glow is not enough

Older glow plugins work as a full-screen effect. They light up everything bright, including the icon buttons in the menu. In Dreamica, MK Glow in Unity 6 made the icons nearly vanish. Unity's official Post Processing Stack v2 only replaces it for the built-in pipeline and no longer applies in URP. If you want glow you control per material, plus dynamic 2D lights, move to URP and use the Bloom built into it.

Step 1: Switch the project to URP

After switching to URP, an old 2D game will almost certainly hit two traps. Both show up as a picture that looks wrong, or exactly the same as before, even though the shaders are fine.

A grey screen: set up a camera stack

If the Game view in the Editor shows nothing but a grey area while the Scene view and a one-off camera render look fine, the problem is not in the shaders but in how the cameras are combined. Old games often have two cameras, one for the world and one for the UI, and the UI camera only clears depth. That is how cameras are layered in the built-in pipeline, and URP does not support it. It needs an explicit camera stack: one camera is the Base, the others are Overlays added to its stack. Set it up in every scene where cameras are layered.

Sprites ignore lights: switch to the 2D renderer

Sprites with a lit shader ignore every light if the regular URP renderer is active. It does not compute 2D lights at all. Switch to the 2D renderer. Even that one draws no lighting until the scene has at least one 2D light. When something looks exactly the same after a change, first check which renderer is running.

Particles and old plugins

Particles on old shaders render black, or not at all, under the 2D renderer. Give them a new, simple shader.

Watch the Asset Store too. During an asset refresh the Editor can apply an "update" to an old plugin on its own, adding metadata and renaming shaders. Materials find their shader by name, so they break silently. After every larger asset refresh in an old project, review all changed files in version control, including the ones you are not working on.

Step 2: Selective glow with HDR colour and Bloom at threshold 1

The goal is for selected objects to glow (in Dreamica the drawn line, lasers, sparks, fireflies and runes) but not the backgrounds or the UI. Bloom in URP blurs and adds everything brighter than its threshold. Set the threshold to 1 and push the colour of the materials that should glow above 1.

line shader:
    colour = texture × vertex colour × tint × intensity    (intensity = 6)

Bloom:
    threshold = 1, intensity = 0.3
    only what ends up brighter than 1 glows

Backgrounds and buttons have colours between 0 and 1, so they do not pass the threshold and stay sharp. That also gets rid of the washed-out icons. The camera must have HDR enabled. Without it the colour is clamped to 1 and nothing glows. The 2D renderer does not show the HDR toggle in the camera inspector, so enable it from a script.

Low threshold, whole frameFull-screen glow: the line, the button rim and bright parts of the background all glow
Threshold 1, HDR materialSelective glow: only the drawn line glows, the button and background stay sharp
An illustration of the principle. Left: full-screen glow with a low threshold. Right: only the material with intensity above 1 glows.

Once glow is under control like this, it does not have to be optional. In Dreamica we removed the glow toggle from the settings; glow is part of the look.

Emission map: only part of the sprite glows

If only part of a sprite should glow, give it an emission map: a second, black and white texture that marks which part of the sprite glows. Only that part gets a colour above 1, so on a rune the core shines, not the whole outline.

A rune, its black and white emission map and the result with a glowing core
Rune sprite, emission map (white glows) and the result after Bloom.

Pulsing in exposure stops

If the glow should pulse, do not multiply the colour linearly. An HDR material can have a brightness around 2 to the power of 8, and a linear multiplier from 0 to 10 makes no visible difference on it. Compute the pulse in exposure stops (EV), the same way as the intensity slider in Unity's HDR colour picker:

colour = tint × 2 ^ ev,   ev oscillates between a minimum and a maximum

Step 3: A dynamic 2D light around the player

Put a 2D point light on the player with a soft flare around it. In Dreamica a single sine wave drives the flare brightness, the light radius and the light intensity together, so they pulse as one. The environment can set the light colour: white in the forest, orange in the swamp, purple in winter, yellow in hell. Switch backgrounds and obstacles to lit shaders, otherwise they ignore the light.

Short lights bring events to life. When the squirrel's cannon fires, an amber light with intensity 3 flashes and fades out over 0.3 seconds, and the muzzle sparks glow. Not everything has to glow: the dust when the acorn disappears is brown on purpose and does not glow.

The same shader on adjoining surfaces

If two backgrounds join up, the source images have the same brightness and there is still a visible seam, check the shaders. In Dreamica the opening background and the endless forest background each used a different shader. One ignored the material tint of 0.733, the other applied it and darkened the forest to 73%. The fix is the same shader on both surfaces.

Two shadersThe border between two forest backgrounds, the lower part is clearly darker
One shaderThe same border, both backgrounds have the same brightness
The border between the opening background and the forest. Left: the lower part darkened by the 0.733 tint that the upper shader ignores.

Surge: blend to a peak instead of adding

On the way into the next world the light and flare surge: 0.6 seconds up, a short hold, 1.4 seconds down, and the flare turns once. If you simply add the surge on top of the pulse, the peak depends on where the sine wave happens to be, and every surge has a different strength. Blend smoothly from the pulse to a fixed peak instead:

weight = surge curve (0 to 1)
value = pulse + (peak - pulse) × weight
Added on topIntensity chart: the pulse and two surges with peaks of different height
Blend to peakIntensity chart: the pulse and two surges reaching the same peak on the dotted line
Two surges at different phases of the pulse (orange and blue). Left: the peaks differ. Right: both reach the same value.

Step 4: A sharp UI for current screens

A UI drawn for the iPhone 7 has small sprites. Dreamica's menu button sprite was 128 × 64 pixels, and on a phone with three times the pixel density it was stretched several times over. Redraw the sprites at a higher resolution: the new button is 800 × 434, and the progress bars grew from 200 × 26 to 800 × 104.

128 × 64Close-up of a blurry button edge
800 × 434Close-up of a sharp button edge with visible texture
The same button at the same on-screen size. Left: the original sprite, right: the new one.

If the UI is built on NGUI, consider a rewrite. NGUI anchors resolve piece by piece across several frames, long anchor chains sometimes settle in the wrong place, and the scale differs between the Editor, a phone and an iPad. In Dreamica fixes did not make it reliable, so we rewrote the UI in uGUI and TextMeshPro. Go screen by screen and keep the old version running next to it as a reference. Scale the UI by height against a reference resolution (434 × 941 points in our case) and respect the screen's safe area, meaning the notch and rounded corners. When the rewrite is done, remove NGUI from the project entirely so you do not end up with two UI systems.

Limits: uncompressed textures

This process only changes the graphics. In Dreamica we did not compress textures, on purpose, because we wanted full sharpness. The price is size: even the original game had 313 MB of textures in memory in the Editor, and it is 299 MB on the App Store. By our estimate compression would shrink the textures to roughly 40 MB. If game size matters to you, that is the first place to save.

For another Unity game from the first line of code to release, see the post on Space Impact Watch; performance in Unity is covered in the post on horde navigation in Unity DOTS. If the game is still on Unity 4, don't open it directly in Unity 6; the safe route is described in how to upgrade a Unity 4 project to Unity 6. If you have an old Unity game that dropped off the stores and you want it back, get in touch.

← More in Dreamica