Rim lighting and realtime shadows for sprites in Unity (2D) – 2/2

posted in: Dev blog | 6

This second part describes how the lighting is achieved, providing that contour when the sprites get close to a light source.

Phew! It has been a while since I wrote the first entry of this topic in the blog. Maybe you want to refresh your memory in this link, where you can learn how the shadow casting is faked, and how that specific, stepped lighting model is achieved for the backgrounds and level sprites. Now let’s get into the outline that we are using to emphasize the light.

Interactivity and style

Those were the reasons to go after an “outline” that would make the borders stand out when a light is cast upon the sprite. It adds to the overall feeling of interactivity -things in the world are alive and react to lights- and adds style instead of just illuminating the whole sprite with a flat lighting model. Let’s see that in action:

In that GIF there are 2 lights: the more obvious one, yellow-ish, emitted from that particle system that resembles fire and moves across the screen, and a red light in front of the “eye” of that robot head floating behind the character -both appear in Trespassers. Both get mixed subtly, both affect elements in their range (the back of the character, and that gas cylinder close). Let’s dive deeper into the shader code.

Light pass

We use an additional ForwardAdd pass on top of a slightly modified Unity’s default Sprite shader.

We are not using the Lighting provided by Unity, so it gets turned off in this shader. Although the code provided does not write to depth buffer, we needed to, so ZWrite is On in Trespassers. The Blending mode is somehow obvious -we want to add on top of the existing fragment-, and we need to specify the “ForwardAdd” light mode tag to get a pass for each pixel light affecting it (see ref).

We will need to pass the lighting color attenuated to the fragment shader (will get interpolated from vertex shader), the light direction and the rotation matrix, so rotated sprites get the proper outline lit.

Vertex code
In line 12 we get the lighting direction for that pass. Unity provides that functionality built-in. The light direction is normalized later on (line 20), after getting the distance to the light (line 19), which is useful to attenuate the lighting color affecting the vertex using linear attenuation (line 24). Explore different attenuations by modifying the lines 23-24 with different formulas.

In lines 16-17 we get the rotation matrix per vertex and store it in one of the fields passed to the fragment shader. Think of the tangent space as a coordinate space for the texture, or, better, check this excellent post in Gamasutra about tangent space. You can do the same process in the fragment shader but, as the comment outlines, for small sprites there should not be a remarkable difference that justifies bloating the fragment code. Anyway, we need to perform our computations in the same coordinate space, the one local to the texture.

Fragment code
The final color will depend on the lights affecting the fragment. But you may wonder: why are we accessing the main texture (which contains the sprite), if, in this pass, we are adding to the existing, not overwriting. The answer is that this fragment is not applied to transparent pixels, which will be many in most of the sprites.

One tricky point here is: how to access the neighbour pixel? How much should you add/subtract from the current UV coordinates to move just one pixel? In the following code you will find “_MainTex_TexelSize“. This is structure is filled in for the texture “_MainTex” by Unity, as long as you declare the following variable before the fragment shader:

This will give you the size of the texel as the name suggests, which is what we are looking for to move from one pixel to another in the following code:

We have to detect the “borders” of the sprite, so we can draw the outline over the previous one. So, we extract information about the elements surrounding the current pixel, with the following goals:

  1. Know if that pixel is in a border == some of its neighbours is transparent (easy in Trespassers)
  2. Estimate the “normal” of that border

The first question is straightforward: if all of the 4 surrounding pixels are opaque -and in Trespassers the vast majority of the sprites don’t have transparency gradients, so pixels are fully opaque or transparent-, then the sum of their alpha values will add up to 4. If lower than that, then one of the neighbours was transparent == we are in a border!

The second question is more ellaborated. Think of the current pixel as placed at a imaginary (0, 0), so the pixels surrounding are placed at left (-1, 0), up (0, 1), right (1, 0) and down (0, -1). Now, in the previous coordinates, replace “1” by “alpha value”, and add everything. That is what the code does in lines 15, 18, 24, 27, one for each coordinate and direction.

If the pixel is in a border, and now that we have the direction for our fake rim light, we can proceed to transform the light’s direction to the tangent space, so everything is in the same coordinate system we are using here (line 5), and apply the color of the light to the current pixel depending on how the light falls upon that border (line 6)

And that’s all. Probably the hardest part was getting the grasp of how the tangent space works -and that it was the one I needed-, and fighting with lighting modes  so I could get a solution for the needs of my project. Nothing new, really.

Caveats and possible circumventions

More artistic control over the rim light. Using a “bump” texture would do. In the code, that’s what we are replicating in realtime from part of the information. But the tricky part here is fetching the sprite if you are using an atlas. I haven’t researched about this topic, but does not sound straightforward. This option could give you small highlights inside opaque areas in the sprite as well.

Use less passes. I would like too, but I don’t know how, if it is possible at all, which I doubt. You will receive a call to that pass for every light affecting the vertex, so you are potentially needing many passes. Sorry, I have no clue here 😉

Use directional lights. Well, directional lights are not used in the rim light. They would feel unnatural, as we have many inner spaces (for instance, rooms in builidings) and would be weird to get lit when no light is directed to you. Also, it may feel a bit annoying if you are always watching everything with that outline lit. but in case you wanted, maybe you can start by adding a pass with a different LightMode. Unity provides information of lights in some built-in variables. You may need an additional pass, though.

Blocking light with obstacles. I suppose you will need to use ComputeShaders or something more ellaborated. With the previous code, if you place a light at some point, walls do not affect it. Sorry!

Some references

Tangent space explanation: Gamasutra article from Siddhart Hedge

Outline shader for sprites in Unity: Nielson post about that topic

First part of this post: in this same web

Where is the code?

I have updated the code in this GitHub repo. It is the same as the used for the shadow casting. Quick note about license (you will find it there anyway):I went for a GNU GPLv3, so everyone can benefit from the work of everyone, if someone else keeps updating it. No credit is needed, but it would be welcomed 😉


Feel free to leave a comment or tweet me (@crazybitstudios).

See ya!

6 Responses

  1. Linxum


    I followed the instructions, and with the GitHub package I managed to create rim light on my pixel art sprites. It looks fantastic, and thanks so much for taking the Time to write this article, it made a huge difference.

    The only problem I’m having is that it doesn’t seem able to mix two lights sources, it chooses between one or the other depending on which one is closer (and “turns off” the rim of the former one). Any ideas? Thanks!

    • Player1


      Thanks for dropping by. Regarding your question, I don´t remember exactly, but I think it should work fine with multiple lights in “Forward” rendering mode.
      Check that you are using that rendering mode AND you have set the number of lights per vertex to more than 1 (there should be an option in Unity3D Quality´s Settings, if I remember correctly)

      Thanks, and enjoy the shader!

  2. Matt

    Great share. Any chance this could be converted into an Amplify compatible shader?
    I am trying to replicated it myself so i can build on top of it, but I am having a hard time.


    • Player1

      Hi Matt,

      Not sure about what are the requirements for making it compatible with Amplify. I remember using Amplify for the post effects in the game, but I don’t know how it has changed over the last years. Feel free to download and edit and use it for your own games, though.

      As a side note, Unity has been leveraging a set of sprite tools that will render my code “worthless” at some point. Take a look here:
      Unity documentation – Render pipelines: 2D Light Properties

Leave a Reply

Your email address will not be published.