RSS Feed IDS on YouTube IDS on Facebook IDS on YouTube

Writing a SpriteLamp Shader in Unity

posted on January 16, 2014 at 2:23 pm by Steve Karolewics

A 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:

A zombie lit with the help of SpriteLamp

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

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:

shader-errors

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.

Shader "Custom/Bare Bones"
{
    Properties
    {
        _MainTex ("Diffuse Texture", 2D) = "white" {}
    }
    SubShader
    {
        AlphaTest NotEqual 0.0
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            // User-specified properties
            uniform sampler2D _MainTex;

            struct VertexInput
            {
                float4 vertex : POSITION;
                float4 uv : TEXCOORD0;
            };

            struct VertexOutput
            {
                float4 pos : POSITION;
                float2 uv : TEXCOORD0;
            };

            VertexOutput vert(VertexInput input) 
            {
                VertexOutput output;
                output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
                output.uv = float2(input.uv);
                return output;
            }

            float4 frag(VertexOutput input) : COLOR
            {
                float4 diffuseColor = tex2D(_MainTex, input.uv);
                return diffuseColor;
            }

            ENDCG
        }

        Pass
        {    
            Tags { "LightMode" = "ForwardAdd" } 

            CGPROGRAM

            #pragma vertex vert  
            #pragma fragment frag 

            #include "UnityCG.cginc"

            // User-specified properties
            uniform sampler2D _MainTex;

            struct VertexInput
            {
                float4 vertex : POSITION;
                float4 uv : TEXCOORD0;
            };

            struct VertexOutput
            {
                float4 pos : POSITION;
                float2 uv : TEXCOORD0;
            };

            VertexOutput vert(VertexInput input) 
            {
                VertexOutput output;
                output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
                output.uv = float2(input.uv);
                return output;
            }

            float4 frag(VertexOutput input) : COLOR
            {
                float4 diffuseColor = tex2D(_MainTex, input.uv);
                return diffuseColor;
            }

            ENDCG
        }
    }
}

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.

Pass
{	
    Tags { "LightMode" = "ForwardBase" }

    CGPROGRAM

    #pragma vertex vert  
    #pragma fragment frag 

    #include "UnityCG.cginc"

    // User-specified properties
    uniform sampler2D _MainTex;

    struct VertexInput
    {
        float4 vertex : POSITION;
        float4 color : COLOR;
        float4 uv : TEXCOORD0;
    };

    struct VertexOutput
    {
        float4 pos : POSITION;
        float4 color : COLOR;
        float2 uv : TEXCOORD0;
    };

    VertexOutput vert(VertexInput input) 
    {
        VertexOutput output;
        output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
        output.color = input.color;
        output.uv = float2(input.uv);
        return output;
    }

    float4 frag(VertexOutput input) : COLOR
    {
        float4 diffuseColor = tex2D(_MainTex, input.uv);

        float3 ambientLighting = float3(UNITY_LIGHTMODEL_AMBIENT) *
            float3(diffuseColor) * float3(input.color);
        return float4(ambientLighting, diffuseColor.a);
    }

    ENDCG
}

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:

ambient-lighting

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:

Properties
{
    _MainTex ("Diffuse Texture", 2D) = "white" {}
    _SpecColor ("Specular Color", Color) = (1, 1, 1, 1) 
    _Shininess ("Shininess", Float) = 10
}

It’s easy to get the intention of “shininess” reversed. Smoother surfaces have larger values for this, which result in smaller specular highlights.

Pass
{    
    Tags { "LightMode" = "ForwardAdd" }
    Blend One One // additive blending

    CGPROGRAM

    #pragma vertex vert  
    #pragma fragment frag 

    #include "UnityCG.cginc"

    // User-specified properties
    uniform sampler2D _MainTex;
    uniform float4 _SpecColor; 
    uniform float _Shininess;
    uniform float4 _LightColor0;

    struct VertexInput
    {
        float4 vertex : POSITION;
        float4 color : COLOR;
        float4 uv : TEXCOORD0;
    };

    struct VertexOutput
    {
        float4 pos : POSITION;
        float4 color : COLOR;
        float2 uv : TEXCOORD0;
        float4 posWorld : TEXCOORD1
    };

    VertexOutput vert(VertexInput input) 
    {
        VertexOutput output;
        output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
        output.posWorld = mul(_Object2World, input.vertex);
        output.color = input.color;
        output.uv = float2(input.uv);
        return output;
    }

    float4 frag(VertexOutput input) : COLOR
    {
        float4 diffuseColor = tex2D(_MainTex, input.uv);

        // Sprites are screen-aligned, so the normal points toward the screen
        float3 normalDirection = float3(0.0f, 0.0f, -1.0f);

        // For orthographic cameras, the view direction is always known
        float3 viewDirection = float3(0.0f, 0.0f, -1.0f);

        float3 vertexToLightSource = float3(_WorldSpaceLightPos0 - input.posWorld);
        float distance = length(vertexToLightSource);

        float attenuation = 1.0 / distance; // Linear attenuation 
        float3 lightDirection = normalize(vertexToLightSource);

        // Compute diffuse part of lighting
        float normalDotLight = dot(normalDirection, lightDirection);
        float3 diffuseReflection = float3(diffuseColor) * input.color * attenuation *
            float3(_LightColor0) * max(0.0f, normalDotLight);

        // Compute specular part of lighting
        float3 specularReflection;
        if (normalDotLight < 0.0)
        {
            // Light source is on the wrong side, so there's no specular reflection
            specularReflection = float3(0.0, 0.0, 0.0);
        }
        else
        {
            specularReflection = attenuation * float3(_LightColor0) * float3(_SpecColor) * input.color *
                pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), _Shininess);
        }

        return float4(diffuseReflection + specularReflection, diffuseColor.a);
    }

    ENDCG
}

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.

phong-lighting

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:

ExampleHead_Normal

Like with Phong illumination, for normal maps we have to add to our shader properties:

Properties
{
    _MainTex ("Diffuse Texture", 2D) = "white" {}
    _Normal ("Normal", 2D) = "bump" {}
    _SpecColor ("Specular Material Color", Color) = (1,1,1,1) 
    _Shininess ("Shininess", Float) = 10
}

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:

// List of external variables pulled into shader:
uniform sampler2D _MainTex;
uniform sampler2D _Normal;
uniform float4 _SpecColor;
uniform float _Shininess;
uniform float4 _LightColor0;

// Computing the normal:
float3 normalDirection = (tex2D(_Normal, input.uv).xyz - 0.5f) * 2.0f;
normalDirection = float3(mul(float4(normalDirection, 1.0f), _World2Object));
normalDirection.z *= -1;
normalDirection = normalize(normalDirection);

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.

normal-mapped

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:

ExampleHead_Depth
// Shader properties:
Properties
{
    _MainTex ("Diffuse Texture", 2D) = "white" {}
    _Normal ("Normal Map", 2D) = "bump" {}
    _Depth ("Depth Map", 2D) = "gray" {}
    _SpecColor ("Specular Material Color", Color) = (1,1,1,1) 
    _Shininess ("Shininess", Float) = 10
    _AmplifyDepth ("Amplify Depth", Float) = 1
}

// User-specified properties:
uniform sampler2D _MainTex;
uniform sampler2D _Normal;
uniform sampler2D _Depth;
uniform float4 _LightColor0;
uniform float4 _SpecColor; 
uniform float _Shininess;
uniform float _AmplifyDepth;

// Vertex to light source calculation:
float depthColor = (tex2D(_Depth, input.uv).x - 0.5f) * 2.0f;
float3 posWorld = float3(input.posWorld);
posWorld.z -= depthColor * _AmplifyDepth;
float3 vertexToLightSource = float3(_WorldSpaceLightPos0) - posWorld;

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.

depth-mapped

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:

bad-depth

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:

// Shader properties:
Properties
{
    _MainTex ("Diffuse Texture", 2D) = "white" {}
    _Normal ("Normal", 2D) = "bump" {}
    _Depth ("Depth", 2D) = "gray" {}
    _SpecColor ("Specular Material Color", Color) = (1,1,1,1) 
    _Shininess ("Shininess", Float) = 10
    _AmplifyDepth ("Amplify Depth", Float) = 1
    _CelShadingLevels ("Cel Shading Levels", Float) = 0
}

// User-specified properties:
uniform sampler2D _MainTex;
uniform sampler2D _Normal;
uniform sampler2D _Depth;
uniform float4 _SpecColor; 
uniform float4 _LightColor0;
uniform float _Shininess;
uniform float _AmplifyDepth;
uniform float _CelShadingLevels;

// The end of the fragment shader:
// Compute diffuse part of lighting
float normalDotLight = dot(normalDirection, lightDirection);
float diffuseLevel = attenuation * max(0.0f, normalDotLight);

// Compute specular part of lighting
float specularLevel;
if (normalDotLight < 0.0f)
{
    // Light is on the wrong side, no specular reflection
    specularLevel = 0.0f;
}
else
{
    // For orthographic cameras, the view direction is always known
    float3 viewDirection = float3(0.0f, 0.0f, -1.0f);
    specularLevel = attenuation * pow(max(0.0, dot(reflect(-lightDirection, normalDirection),
        viewDirection)), _Shininess);
}

// Add cel-shading if enough levels were specified
if (_CelShadingLevels >= 2)
{
    diffuseLevel = floor(diffuseLevel * _CelShadingLevels) / (_CelShadingLevels - 0.5f);
    specularLevel = floor(specularLevel * _CelShadingLevels) / (_CelShadingLevels - 0.5f);
}

float3 diffuseReflection = float3(diffuseColor) * input.color * float3(_LightColor0) * diffuseLevel;
float3 specularReflection = float3(_LightColor0) * float3(_SpecColor) * input.color * specularLevel;
return float4(diffuseReflection + specularReflection, diffuseColor.a);

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:

cel-shading

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

better-cel-shading2

The Finished Shader

Putting everything together, the final shader looks like this:

629kl
// Shader for Unity integration with SpriteLamp
// Written by Steve Karolewics &amp; Indreams Studios
Shader "Custom/SpriteLamp"
{
    Properties
    {
        _MainTex ("Diffuse Texture", 2D) = "white" {}
        _Normal ("Normal", 2D) = "bump" {}
        _Depth ("Depth", 2D) = "gray" {}
        _SpecColor ("Specular Material Color", Color) = (1,1,1,1) 
        _Shininess ("Shininess", Float) = 10
        _AmplifyDepth ("Amplify Depth", Float) = 1
        _CelShadingLevels ("Cel Shading Levels", Float) = 0
    }

    SubShader
    {
        AlphaTest NotEqual 0.0
        Pass
        {    
            Tags { "LightMode" = "ForwardBase" }

            CGPROGRAM

            #pragma vertex vert  
            #pragma fragment frag 

            #include "UnityCG.cginc"

            // User-specified properties
            uniform sampler2D _MainTex;

            struct VertexInput
            {
                float4 vertex : POSITION;
                float4 color : COLOR;
                float4 uv : TEXCOORD0;    
            };

            struct VertexOutput
            {
                float4 pos : POSITION;
                float4 color : COLOR;
                float2 uv : TEXCOORD0;
            };

            VertexOutput vert(VertexInput input) 
            {
                VertexOutput output;

                output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
                output.color = input.color;
                output.uv = float2(input.uv);
                return output;
            }

            float4 frag(VertexOutput input) : COLOR
            {
                float4 diffuseColor = tex2D(_MainTex, input.uv);

                float3 ambientLighting = float3(UNITY_LIGHTMODEL_AMBIENT) * float3(diffuseColor) *
                    float3(input.color);
                return float4(ambientLighting, diffuseColor.a);
            }

            ENDCG
        }

        Pass
        {    
            Tags { "LightMode" = "ForwardAdd" }
            Blend One One // additive blending 

            CGPROGRAM

            #pragma vertex vert  
            #pragma fragment frag 

            #include "UnityCG.cginc"

            // User-specified properties
            uniform sampler2D _MainTex;
            uniform sampler2D _Normal;
            uniform sampler2D _Depth;
            uniform float4 _SpecColor; 
            uniform float4 _LightColor0;
            uniform float _Shininess;
            uniform float _AmplifyDepth;
            uniform float _CelShadingLevels;

            struct VertexInput
            {
                float4 vertex : POSITION;
                float4 color : COLOR;
                float4 uv : TEXCOORD0;
            };

            struct VertexOutput
            {
                float4 pos : POSITION;
                float4 color : COLOR;
                float2 uv : TEXCOORD0;
                float4 posWorld : TEXCOORD1;
            };

            VertexOutput vert(VertexInput input)
            {
                VertexOutput output;

                output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
                output.posWorld = mul(_Object2World, input.vertex);

                output.uv = float2(input.uv);
                output.color = input.color;
                return output;
            }

            float4 frag(VertexOutput input) : COLOR
            {
                float4 diffuseColor = tex2D(_MainTex, input.uv);

                // To compute the correct normal: 
                //   1) Get the pixel value from the normal map
                //   2) Subtract 0.5 and multiply by 2 to convert from the range 0...1 to -1...1
                //   3) Multiply by world to object matrix, to handle rotation, etc
                //   4) Negate Z so that lighting works as expected (sprites further away from the camera than
                //      a light are lit, etc.)
                //   5) Normalize
                float3 normalDirection = (tex2D(_Normal, input.uv).xyz - 0.5f) * 2.0f;
                normalDirection = float3(mul(float4(normalDirection, 1.0f), _World2Object));
                normalDirection.z *= -1;
                normalDirection = normalize(normalDirection);

                // To adjust depth:
                //   1) Get the depth value from the depth map
                //   2) Subtract 0.5 and multiply by 2 to convert from the range 0...1 to -1...1
                //   3) Multiply by the amplify depth value, and subtract from the fragment's z position
                float depthColor = (tex2D(_Depth, input.uv).x - 0.5f) * 2.0f;
                float3 posWorld = float3(input.posWorld);
                posWorld.z -= depthColor * _AmplifyDepth;
                float3 vertexToLightSource = float3(_WorldSpaceLightPos0) - posWorld;
                float distance = length(vertexToLightSource);

                // The values for attenuation and lightDirection are assuming point lights
                float attenuation = 1.0 / distance; // Linear attenuation is good enough for now
                float3 lightDirection = normalize(vertexToLightSource);

                // Compute diffuse part of lighting
                float normalDotLight = dot(normalDirection, lightDirection);
                float diffuseLevel = attenuation * max(0.0f, normalDotLight);

                // Compute specular part of lighting
                float specularLevel;
                if (normalDotLight < 0.0f)
                {
                    // Light is on the wrong side, no specular reflection
                    specularLevel = 0.0f;
                }
                else
                {
                    // For orthographic cameras, the view direction is always known
                    float3 viewDirection = float3(0.0f, 0.0f, -1.0f);
                    specularLevel = attenuation * pow(max(0.0, dot(reflect(-lightDirection, normalDirection),
                        viewDirection)), _Shininess);
                }

                // Add cel-shading if enough levels were specified
                if (_CelShadingLevels >= 2)
                {
                    diffuseLevel = floor(diffuseLevel * _CelShadingLevels) / (_CelShadingLevels - 0.5f);
                    specularLevel = floor(specularLevel * _CelShadingLevels) / (_CelShadingLevels - 0.5f);
                }

                float3 diffuseReflection = float3(diffuseColor) * input.color *
                    float3(_LightColor0) * diffuseLevel;
                float3 specularReflection = float3(_LightColor0) * float3(_SpecColor) *
                    input.color * specularLevel;
                return float4(diffuseReflection + specularReflection, diffuseColor.a);
             }

             ENDCG
        }
    }
    // The definition of a fallback shader should be commented out 
    // during development:
    // Fallback "Transparent/Diffuse"
}

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!

19 Oranges

Comments hidden
  1. 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!

  2. 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.

  3. 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

  4. Steve Karolewics
    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.

  5. 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.

  6. 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

  7. Steve Karolewics
    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.

  8. 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);

  9. 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?

  10. Steve Karolewics
    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).

  11. 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

  12. Steve Karolewics
    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.

  13. 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

  14. I have the “purple rectangle” error on windows. Anybody knows why?

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

  16. 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.

  17. Is this better than the Standard Shader in Unity 5?

  18. Thanks so much for the tutorial and the shader! I hope you guys make the next episode.

Add Comment Register



Leave a Reply

Your email address will not be published. Required fields are marked *


six − = 3

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>