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

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

posted in: Dev blog | 0

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

Phew! It has been a while since I wrote the first entry 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 reason 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.

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 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!

Leave a Reply