Writing a SpriteLamp Shader in Unity
posted on January 16, 2014 at 2:23 pm by Steve KarolewicsA few months ago, Justin and I began investigating ways to make our next game (in Unity3D) look great. We knew we wanted to stick with 2D art, but didn’t want to tacitly accept the “conventional” expectations associated with 2D art in games.
SpriteLamp
We discovered SpriteLamp, which essentially allows you to generate dynamic lighting on pixel art. It accomplishes this by producing normal maps, depth maps, and anisotropy maps for use in shaders. All you provide are 2-5 “lighting profiles” of what an object would look like lit from a specific direction (top, bottom, left, right, or front). This animation sort of sells itself:

Courtesy of the SpriteLamp Kickstarter
We encourage anyone interested to check out the Kickstarter or SnakeHillGames for more information. SpriteLamp was successfully funded, and we’ve received beta access. The tool, even in its beta state, is very usable, and has UI that is easy enough to understand for now:

The UI for SpriteLamp
Although we’re not artists, even we could see how exciting this would be to get working in Unity. SpriteLamp’s developer, Finn Morgan, said that a shader for Unity will be provided later, but we decided that we couldn’t wait, so we wrote it ourselves.
Shaders in Unity
For those unfamiliar with how shaders work in Unity, here are some resources that helped out a lot for us:
Another important aspect to keep in mind is that if your shader has errors, it’s easiest to see the errors by viewing the shader file itself in Unity’s inspector window:

Sometimes Unity’s Console window will show all shader errors, but I’ve found the Inspector for the shader to be more reliable.
With all of that in mind, let’s get started. I figure it will be more valuable to talk through the various aspects of the shader, rather than just provide the shader in its entirety (though if you just want that, check the end of the article).
A Bare Bones Cg Shader
Let’s start with the minimal amount of work, to better understand the structure of a Unity shader, since it was pretty overwhelming for me at first, especially as it relates to lighting. If you’re familiar with the structure of shaders and how Unity handles multiple lights, then feel free to jump to the next section.
This might seem a bit overwhelming already for someone new to shaders in Unity, but it’s a great starting point. Let’s talk about what is going on.
Shader Header
On line 1, we specify the name of the shader, as viewed when selecting a shader for a material. Using slashes gets you nested folders in the shader dropdown. On lines 3-6, the shader properties specify what data you can set outside the shader that will be brought in. See this documentation for more information on shader properties. Since we’re using the new Unity 2D features, _MainTex is required if you’re going to use SpriteRenderer. On line 9, we specify that pixels with an alpha of 0 should be ignored.
ForwardBase Pass
We’re now describing one pass of our shader, referred to as “ForwardBase”. This is where ambient lighting, the first directional light, per-vertex lights, and lights using spherical harmonics are handled. This Unity reference page explains the various Pass tags in more detail, and this page explains how Unity handles multiple lights in shaders.
Then we begin writing our Cg shader, which occurs between CGPROGRAM and ENDCG. We specify the function names that we’ll use for our vertex and fragment shaders. Then we state the data that we’ll bring in from outside the shader. These variables must be named the same as values specified in the Properties section. Next, structs are defined for the data that our vertex shader will receive, and what it will output. The output of the vertex shader is the same data received by the fragment shader (after interpolation of that data occurs). For now, we’re just using the vertex position and texture coordinates.
In our vertex shader, we simply pass the text coordinates through, but we multiply the position by the model*view*projection matrix. This converts the vertex position from object space to screen space. In our fragment shader, we simply get the pixel from the main texture and return that color.
ForwardAdd Pass
This pass is used for per-pixel lights, and it should look really familiar. For now it’s the same, but that will quickly change.
Ambient Lighting in ForwardBase
For our uses, we only wanted to focus on ambient lighting in our ForwardBase pass. However, if you want to add directional lights or per-vertex lights, this Cg shader page will be helpful.
With this shader pass, we’ve added color to the data we receive in the vertex shader and pass to the fragment shader. The field specified by COLOR correlates to the color set via SpriteRenderer. Factoring that in, along with UNITY_LIGHTMODEL_AMBIENT (which is the ambient light color specified in your project’s render settings), we get ambient-lit sprites that we can further color via SpriteRenderer:

We’re now done with the ForwardBase pass, so all code past this point is focusing on the ForwardAdd pass.
Phong Illumination
Phong illumination (your standard ambient+diffuse+specular lighting) has been described in many places (such as Wikipedia), but it’s an important step to getting SpriteLamp integrated properly in Unity. To prepare for this, we have to add some shader properties:
It’s easy to get the intention of “shininess” reversed. Smoother surfaces have larger values for this, which result in smaller specular highlights.
On line 4, we specify a blend state for additive blending. By using additive blending, we allow multiple lights to contribute to lighting, instead of each light overwriting the previous one.
Our vertex shader now also outputs “posWorld”, which is the vertex position in world coordinates (unlike output.pos, which is in screen coordinates). We’ll need this for computing light strength. Although it’s not a texture coordinate, we bind it to TEXCOORD1 because we choose how to interpret it in the fragment shader.
Fragment Shader
In order to compute Phong illumination, you need to know the normal for the surface (or else you don’t know how strong a light is hitting a surface). Since we’re working with Unity 2D sprites, the normal is always (0, 0, -1), pointing toward the screen. Additionally, the view direction (which points from the fragment to the camera) is needed to compute specular highlights. Since we have an orthographic camera, this is also a known value.
_LightColor0 is a built-in value provided by Unity, and is pretty self-explanatory. This is also the first time we encounter lighting attentuation, which specifies how light strength descreases over time. This typically follows an inverse quadratic curve, but for now we’re using inverse linear. There’s nothing wrong with either, but inverse linear provides more light.
It’s important to note that this code is intended for point lights only. If you want to use directional lights, then the attenuation and lightDirection are calculated differently. If you want to use spot lights, cookie attenuation needs to be added.
The rest of the new code in this shader is just following the Phong illumination model, so I won’t explain any further.

Normal Maps
Now that we have standard lighting implemented, it’s time to take advantage of SpriteLamp! The first (and most important) aspect to integrate is the normal map. For the head above, our normal map looks like this:

Like with Phong illumination, for normal maps we have to add to our shader properties:
For the normal map, “bump” refers to a default texture where the red and green channels (which correspond to the x and y components of the normals) are 128, and the blue channel (the normal’s z component) is 255. The range of values in a normal map is from -1 to 1, so by default a normal map specifies normals of (0, 0, 1). This is why most normal maps appear blue.
As you might expect, the only thing that changes when incorporating normals is the normalDirection variable. This still requires a few changes throughout the shader, though:
There’s a lot going on in those few lines that compute the normal. Since we’re getting the normal from a texture, we have to convert from color coordinates to normal coordinates. Colors range from 0 to 1, while normals range from -1 to 1. This is handled on line 9.
Next, we multiply the normal by the “world to object” matrix. This is necessary because that matrix contains the transform for things such as rotated sprites. Without this line, the lighting wouldn’t change as you rotate a sprite around a light!
As mentioned above, the default normal value is (0, 0, 1). However, as you’ll recall from the Phong illumination shader, we used (0, 0, -1), so we negate the z component. Finally, we normalize the normal.

Depth Maps
Another feature that SpriteLamp provides is depth maps, which adjust the depth of the fragment. For sprites, this translates to adjusting the Z position, but more generally it adjusts the position along the normal that would be computed in the vertex shader. We’re just taking shortcuts because we’re using sprites (and also since shaders have an instruction limit). This depth map generated by SpriteLamp adds some definition to the ear and jawline, and adds rounding to the edges of the head and face:

As you probably expected, we added the depth map to the shader properties. We’re interpreting the depth map to be able to both add and subtract depth, so the range 0 to 1 for color maps to -1 to 1 for depth. This means that our texture should be “gray” by default, and we do the same computation as we did for normals to convert to the -1 to 1 space.
After getting the depth adjustment, we then subtract it from our posWorld.z, factoring in the “Amplify Depth” setting. We’re subtracting because our camera is looking in the positive Z direction, and the brighter areas of the depth map are “closer”, which means moving in the negative Z direction.

The difference in “amplify depth” settings is very noticeable, but you have to be careful to not increase it so much that the sprite will be “within” a light:

The unlit parts in the center of the head have final depth values that are closer to the camera than the light.
A Future Improvement
Some of you may have noticed that we’re only focusing on the x component for the depth map. And for the normal map, we only focused on the x, y, and z components. While we haven’t implemented it, one improvement that we’re considering (and we hope you do too) is to combine the normal map and depth map, so that the alpha value of the bitmap is the depth value, reducing the number of texture lookups needed.
Cel-Shading
As Finn Morgan pointed out in his recent blog post, adding cel-shading is a pretty simple technique:
Note that now we compute the diffuse and specular “level” (before color is factored in), apply cel-shading to that, and then add the color-based computations in. If you just added the cel-shading to the end of the previous shader, you would end up with per-component cel-shading which generally looks terrible:

However, with the changes to apply cel-shading before the color is added, we get a much better result:

The Finished Shader
Putting everything together, the final shader looks like this:

You can also download it directly here: http://indreams-studios.com/SpriteLamp.shader
Conclusion
There are still plenty of features that we haven’t implemented for our shader, such as ambient occlusion, anisotropy maps, self-shadowing, and wraparound lighting. We’re content with where we are at this point though, since we can continue making progress on our game visually, and there’s always time to improve the shader later. If you end up using (or improving) this shader, please let us know! We’d love to learn that we’re helping out other developers.
Edit: By request, we added the MIT license to the final shader file, so feel free to use it unrestricted!
February 10, 2014 at 2:00 am
Hi there!
Nice work with the SpriteLamp shader. I am currently starting a game which could take great advantage from dynamic lighting, but unfortunately I know very little about shading and CG programming.
I have two questions for you:
1) does this shader work for 2D animated sprites? If so, how exactly? Let’s say I have a spritesheet with all my sprites, do I need to make an identical normal map? And if so, how can the animation controller pick the corresponding normal map to the frame it’s currently playing? Plus, do I need to define the normal map texture as a normal map or as a sprite (in the same way of the diffuse texture)?
2) the shader gives three warnings upon compile, is this normal?
Warnings are: Program ‘vert’ incorrect number of arguments to numeric-type constructor at line 43.
The errors seem to be related to directX-Direct3d.
Thanks!
February 10, 2014 at 10:18 am
Hi Federico,
1) The SpriteLamp Kickstarter discussed this: https://www.kickstarter.com/projects/finnmorgan/sprite-lamp-dynamic-lighting-for-2d-art#project_faq_73961
We haven’t played around with animated sprite integration yet, but if you have a spritesheet, you should be able to put the whole sheet into SpriteLamp as one image, along with the lighting profiles, and get a single normal map for the spritesheet. I’m not sure how easy it will be to tell the shader the right UVs for the normal map, but it isn’t an intractable problem.
2) Yeah, those warnings are ignorable. They stem from doing things like float3(someFloat4Variable). You can fix these by doing someFloat4Variable.rgb instead, but even then I wasn’t able to get rid of those warnings.
Hope that helps! Let us know if you have any other questions.
April 10, 2014 at 7:23 am
Hi,
How would one adapt this shader taking into account a 3D environment? It seems to be setup for an orthographic view and my knowledge of shaders, which I’ve only delved into recently leaves me stuck in trying to extend this. I’ve had some results but they’re not ideal and caused the way the lighting affects the shader to break. I’d like to achieve the results of light affecting the sprite where it’s rotation might be around the X or Y axis, which light doesn’t affect with this shader, it only affects it on the Z axis. How can I extend this? I’m sure it’s something trivial, but I can’t figure it out with the above shader implementation.
Here’s a scenario, a retro FPS in 3D space with 2D sprites. The lighting works as expected, but not when the sprite is rotated around the X or Y axis, it works obviously as intended when it’s rotated on the Z axis. Here’s a GIF demonstrating how it’s not affecting it the corners of this wall and the floor: http://i.imgur.com/J6aHiHc.gif
Any tips would be appreciated and hopefully what I’m describing makes sense
April 10, 2014 at 8:04 am
Hi Henry,
Great question! I haven’t played around with doing this for 3D, but I suspect there are 2 big things to adjust: 1) The normal direction – we assume we’re facing toward positive Z, which makes the math simpler for the 2D case. But you’ll want to instead adjust the normal from whatever it currently is. To do this, you’d add a VertexInput field for NORMAL, and pass it through to VertexOutput. Then, normalDirection will adjust from the provided normal instead of assuming (0,0,1). Looking at other shader implementations for normal mapping would help you here. 2) The view direction – for an orthographic view, it always points (0,0,-1), but that isn’t the case for 3D. Take a look at the link in the article for “Cg Programming in Unity”, they should be able to help.
Hope that helps! Happy shader writing.
April 10, 2014 at 11:19 am
Thanks for the information, I attempted that and read through a bit of the “CG Programming in Unity” and I’ve managed some progress, it’s not entirely correct yet, for some reason the tiles are being affected in the opposite way with the light as you can see in this image
http://i.imgur.com/roZOTNj.jpg
The one side of the tile is lit and the other is less lit as it should be according to how I made the normals from the light profiles, but it should be the opposite way around, some of my modifications has swapped it around, definitely has to do with the direction of things with the views/normals. The lit and less lit areas should be swapped around, otherwise it’s almost there. My normal maps themselves aren’t very good because the light profiles I made were kind of bad, it was a quick test. I’m on the right track I think. It’s late here now so tomorrow I’ll see if I can figure out why it’s doing what it is (swapping) and I’ll make a new post with what I’ve changed. Shaders are new to me so it’s a good learning experience, thanks for the initial work, it has taught me a lot.
May 21, 2014 at 5:17 am
Hey man. Good job on the shader. I’ve actually been playing around with it with Sprite Lamp. It works really great but for som reason, when the light (spot or point) roles of the sprite the edges of the light are sharp and square shape. I don’t know much about programming shader. Do you have any idea why this occurs?
http://puu.sh/8V9JB.png
Best,
Mads
May 26, 2014 at 2:14 pm
Hi Mads,
This occurs because of a conjunction of light attenuation and optimizations in Unity. For speed benefits, only vertices/fragments within the range of the light are affects by a given light. You can increase the range of a light to help with this. Light attenuation (the “1.0 / distance” part in this tutorial’s shader) affects how much a light affects objects as the distance increases. In the real world, it follows a generally inverse quadratic formula. Ideally, you’d factor in the range of the light, so that the attenuation goes to zero as you approach the end of the light’s range (and solving the issue you are seeing). For the purposes of this tutorial I didn’t try to figure out how to get the range of a given light in a shader, but that is where you should start.
June 8, 2014 at 11:42 pm
I did a clean install of the latest version of Unity. I created a new project. I imported the finished SpriteLamp.shader.
I got the following error:
incorrect number of arguements to numeric-type constructor [d3d11] line 53
The line throwing the error reads:
output.uv = float2(input.uv);
June 9, 2014 at 8:57 pm
I would love to use this shader, but when I tell my sprite to use it, it turns into a purple rectangle. The built-in sprite shaders work, and my other custom sprite shaders work.
I’m using a fresh install of Unity 4.5.0f6 (the current version as of this posting).
Is there some trick to using this that I’m missing?
Would you mind sharing an example project?
June 9, 2014 at 9:32 pm
Fred and flackl – are both of you running Unity on a Mac? I’ve tested this on Windows but I don’t have access to a Mac to try there. My only suggestion (found from here) would be to expand any float2/float3/float4 that are swizzled to the expanded form, such as converting float2(input.uv) to float2(input.u, input.v).
June 19, 2014 at 12:03 am
Dear Steve and Justin,
Excellent work. I would highly appreciate, if you included some license for you shader code.
I suggest MIT license, however, you are free to choose your own.
http://opensource.org/licenses/MIT
Best regards, Jarmo
June 22, 2014 at 1:17 pm
Hi Jarmo,
Thanks for the suggestion! I’ve added the MIT license to the final shader file and updated the post to indicate that.
September 1, 2014 at 12:15 am
Hi,
good job on the shader and the post. Hoewever I can’t make it work with unity sprites. And I see in the tutorial there are screenshots of pristerenderer so I don’t know where I am making error. Just made new material, assigned to the spriterenderer and then I have just pink quad. Please help me, what am I doing wrong?
Thanks,
Jan
September 1, 2014 at 2:22 am
I have the “purple rectangle” error on windows. Anybody knows why?
September 2, 2014 at 11:01 am
so after some tests I found out, that the problem occurs in unity 4.6 and higher, in 4.5.3 it works, 4.6+ does the purple rectangle and the spriterenderer component is showing message “Material does not have a _MainTex texture property. It is required for SpriteRenderer.” so i guess it would be good idea to update the shader because 4.6 is coming out really soon. (Sure i know there really is the _MainTex, just saying what it displays.)
Anyway, good work guys and thank you!
September 3, 2014 at 7:48 pm
Hi Jan,
Thanks for the kind words on the post. We’ll try to update the post for 4.6 once it is publicly available. If you ever experience other shaders visible as bright pink, that typically indicates a shader error, and Unity 4.5 introduced better shader error reporting, which should be visible in the console, but if not can be seen when selecting the problematic shader.
January 11, 2015 at 6:09 pm
Is this better than the Standard Shader in Unity 5?
June 7, 2015 at 9:26 pm
Thanks so much for the tutorial and the shader! I hope you guys make the next episode.
July 18, 2016 at 8:25 pm
Fixed errors in case anyone is interested
https://docs.google.com/document/d/1ZbQd8k0VlAmCWSxLMjulzK6Xyxtor7aQszTWoHzANpM/edit