How do you make lights affect 2D sprites in a 3D environment using Unity's URP render pipeline?

You might have to actually jump into the code and Frankenstein it - I don’t even have a clue though, how that would look or work. I tried doing this a little and realized it wasn’t worth the time for my game. I spent a day or so tinkering around trying to add specular lighting to the 2d renderer…

For me, I would simply put simple-lit material on your sprite and use 3d lights. I would probably use a normal map with the sprites though - even if it is a simple up pointing normal so they are lit better by lights from above. You would have to play with it. I wanted something similar, but decided I wanted 2d lights and their “fog” more than 3d. I’m actually still using 3d objects, but I’m using the 2d render asset. Its fine so long as you don’t rotate the camera.

Hi @VEWO , did you find a solution about this?
I have the same question.

convert sprite into mesh and then use material

So I was able to find a hack to get this kind of thing to work using Stencil on copies of some built-in shaders.

The idea is to tackle this in 2 passes:

  1. Use the Sprites-Default shader to draw the sprite in the world, while adding a stencil to it to mark each pixel shaded by it.
  2. Use the Lit shader from URP, but only filter it to operate on pixels that were drawn into by the sprite shader in the previous pass.

The sprite is drawn normally, unshadowed. The magic happens in the 2nd step.
What this does is basically put up a plane on top of your sprite, which receives shadows via the lit shader, but only draws the pixels that the sprite occupies.

The full details for achieving this:

Create a Shadowed Sprite shader:

// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)

Shader "Sprites/Shadowed"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
        [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
        [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1)
        [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1)
        [PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {}
        [PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Cull Off
        Lighting Off
        ZWrite Off
        Blend One OneMinusSrcAlpha

        Stencil
        {
            Ref 584
            Comp Always
            Pass Replace
            Fail Keep
            ZFail Keep
        }

        Pass
        {
        CGPROGRAM
            #pragma vertex SpriteVert
            #pragma fragment frag
            #pragma target 2.0
            #pragma multi_compile_instancing
            #pragma multi_compile_local _ PIXELSNAP_ON
            #pragma multi_compile _ ETC1_EXTERNAL_ALPHA
            #include "UnitySprites.cginc"
           
            fixed4 frag(v2f IN) : SV_Target
            {
                fixed4 c = SpriteFrag(IN);

                if (c.a == 0)
                {
                    discard;
                }

                return c;
            }
        ENDCG
        }
    }
}

This is just the Sprite-Default shader with the following 2 changes:

  1. Added the stencil which will let other shaders know which pixels on the screen were drawn into by this shader.
  2. Added an intermediate step to the fragment shader which will discard any pixels that draw transparent pixels (The shader runs on every pixel of the sprite, including transparent ones, so unless this is called, the shadow will be applied to the full rect of the sprite instead of just the visible parts of it) (Note: This works because the ‘discard’ keyword also discards Stencil information of the pixel and not just the color. This was an annoyance to me many a-time in the past, but this time it actually works in our favor).

Next, we need to create a ShadowOnly variant of the Lit shader from the URP package:

Shader "Universal Render Pipeline/Lit - Shadows Only"
{
    Properties
    {
        // Specular vs Metallic workflow
        [HideInInspector] _WorkflowMode("WorkflowMode", Float) = 1.0

        [MainColor] _BaseColor("Color", Color) = (1,1,1,1)
        [MainTexture] _BaseMap("Albedo", 2D) = "white" {}

        _Cutoff("Alpha Cutoff", Range(0.0, 1.0)) = 0.5

        _Smoothness("Smoothness", Range(0.0, 1.0)) = 0.5
        _GlossMapScale("Smoothness Scale", Range(0.0, 1.0)) = 1.0
        _SmoothnessTextureChannel("Smoothness texture channel", Float) = 0

        [Gamma] _Metallic("Metallic", Range(0.0, 1.0)) = 0.0
        _MetallicGlossMap("Metallic", 2D) = "white" {}

        _SpecColor("Specular", Color) = (0.2, 0.2, 0.2)
        _SpecGlossMap("Specular", 2D) = "white" {}

        [ToggleOff] _SpecularHighlights("Specular Highlights", Float) = 1.0
        [ToggleOff] _EnvironmentReflections("Environment Reflections", Float) = 1.0

        _BumpScale("Scale", Float) = 1.0
        _BumpMap("Normal Map", 2D) = "bump" {}

        _OcclusionStrength("Strength", Range(0.0, 1.0)) = 1.0
        _OcclusionMap("Occlusion", 2D) = "white" {}

        _EmissionColor("Color", Color) = (0,0,0)
        _EmissionMap("Emission", 2D) = "white" {}

        // Blending state
        [HideInInspector] _Surface("__surface", Float) = 0.0
        [HideInInspector] _Blend("__blend", Float) = 0.0
        [HideInInspector] _AlphaClip("__clip", Float) = 0.0
        [HideInInspector] _SrcBlend("__src", Float) = 1.0
        [HideInInspector] _DstBlend("__dst", Float) = 0.0
        [HideInInspector] _ZWrite("__zw", Float) = 1.0
        [HideInInspector] _Cull("__cull", Float) = 2.0

        _ReceiveShadows("Receive Shadows", Float) = 1.0
        // Editmode props
        [HideInInspector] _QueueOffset("Queue offset", Float) = 0.0

        // ObsoleteProperties
        [HideInInspector] _MainTex("BaseMap", 2D) = "white" {}
        [HideInInspector] _Color("Base Color", Color) = (1, 1, 1, 1)
        [HideInInspector] _GlossMapScale("Smoothness", Float) = 0.0
        [HideInInspector] _Glossiness("Smoothness", Float) = 0.0
        [HideInInspector] _GlossyReflections("EnvironmentReflections", Float) = 0.0
    }

    SubShader
    {
        // Universal Pipeline tag is required. If Universal render pipeline is not set in the graphics settings
        // this Subshader will fail. One can add a subshader below or fallback to Standard built-in to make this
        // material work with both Universal Render Pipeline and Builtin Unity Pipeline
        Tags{"RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "IgnoreProjector" = "True" "Queue"="Transparent"}
        LOD 300

        Stencil
        {
            Ref 584
            Comp Equal
            Pass Keep
            Fail Zero
        }

        // ------------------------------------------------------------------
        //  Forward pass. Shades all light in a single pass. GI + emission + Fog
        Pass
        {
            // Lightmode matches the ShaderPassName set in UniversalRenderPipeline.cs. SRPDefaultUnlit and passes with
            // no LightMode tag are also rendered by Universal Render Pipeline
            Name "ForwardLit"
            Tags{"LightMode" = "UniversalForward"}

            Blend[_SrcBlend][_DstBlend]
            ZWrite[_ZWrite]
            Cull[_Cull]

            HLSLPROGRAM
            // Required to compile gles 2.0 with standard SRP library
            // All shaders must be compiled with HLSLcc and currently only gles is not using HLSLcc by default
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0

            // -------------------------------------
            // Material Keywords
            #pragma shader_feature _NORMALMAP
            #pragma shader_feature _ALPHATEST_ON
            #pragma shader_feature _ALPHAPREMULTIPLY_ON
            #pragma shader_feature _EMISSION
            #pragma shader_feature _METALLICSPECGLOSSMAP
            #pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
            #pragma shader_feature _OCCLUSIONMAP

            #pragma shader_feature _SPECULARHIGHLIGHTS_OFF
            #pragma shader_feature _ENVIRONMENTREFLECTIONS_OFF
            #pragma shader_feature _SPECULAR_SETUP
            #pragma shader_feature _RECEIVE_SHADOWS_OFF

            // -------------------------------------
            // Universal Pipeline keywords
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
            #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
            #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS
            #pragma multi_compile _ _SHADOWS_SOFT
            #pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE

            // -------------------------------------
            // Unity defined keywords
            #pragma multi_compile _ DIRLIGHTMAP_COMBINED
            #pragma multi_compile _ LIGHTMAP_ON
            #pragma multi_compile_fog

            //--------------------------------------
            // GPU Instancing
            #pragma multi_compile_instancing

            #pragma vertex LitPassVertex
            #pragma fragment LitPassFragment

            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitForwardPass.hlsl"
            ENDHLSL
        }

        Pass
        {
            Name "ShadowCaster"
            Tags{"LightMode" = "ShadowCaster"}

            ZWrite On
            ZTest LEqual
            Cull[_Cull]

            HLSLPROGRAM
            // Required to compile gles 2.0 with standard srp library
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0

            // -------------------------------------
            // Material Keywords
            #pragma shader_feature _ALPHATEST_ON

            //--------------------------------------
            // GPU Instancing
            #pragma multi_compile_instancing
            #pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

            #pragma vertex ShadowPassVertex
            #pragma fragment ShadowPassFragment

            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl"
            ENDHLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags{"LightMode" = "DepthOnly"}

            ZWrite On
            ColorMask 0
            Cull[_Cull]

            HLSLPROGRAM
            // Required to compile gles 2.0 with standard srp library
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0

            #pragma vertex DepthOnlyVertex
            #pragma fragment DepthOnlyFragment

            // -------------------------------------
            // Material Keywords
            #pragma shader_feature _ALPHATEST_ON
            #pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

            //--------------------------------------
            // GPU Instancing
            #pragma multi_compile_instancing

            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"
            ENDHLSL
        }

        // This pass it not used during regular rendering, only for lightmap baking.
        Pass
        {
            Name "Meta"
            Tags{"LightMode" = "Meta"}

            Cull Off

            HLSLPROGRAM
            // Required to compile gles 2.0 with standard srp library
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x

            #pragma vertex UniversalVertexMeta
            #pragma fragment UniversalFragmentMeta

            #pragma shader_feature _SPECULAR_SETUP
            #pragma shader_feature _EMISSION
            #pragma shader_feature _METALLICSPECGLOSSMAP
            #pragma shader_feature _ALPHATEST_ON
            #pragma shader_feature _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

            #pragma shader_feature _SPECGLOSSMAP

            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitMetaPass.hlsl"

            ENDHLSL
        }
        Pass
        {
            Name "Universal2D"
            Tags{ "LightMode" = "Universal2D" }

            Blend[_SrcBlend][_DstBlend]
            ZWrite[_ZWrite]
            Cull[_Cull]

            HLSLPROGRAM
            // Required to compile gles 2.0 with standard srp library
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x

            #pragma vertex vert
            #pragma fragment frag
            #pragma shader_feature _ALPHATEST_ON
            #pragma shader_feature _ALPHAPREMULTIPLY_ON

            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/Utils/Universal2D.hlsl"
            ENDHLSL
        }


    }
    FallBack "Hidden/Universal Render Pipeline/FallbackError"
    CustomEditor "UnityEditor.Rendering.Universal.ShaderGUI.LitShader"
}

The only change here is the addition of the stencil that will filter out any pixels that weren’t rendered by the sprite.

Then, all we need is to create materials for the 2 shaders, making sure that the Lit-ShadowsOnly material uses the Transparent surface type, Multiply Blending mode and a priority that has it take effect AFTER the sprite shader.


(Important note: Up to URP version 7.4.0 there was a bug where the Priority wouldn’t be added correctly to offset the rendering queue of the shader, and would have the (-50, 50) priority range in the inspector translated to the (3000, 3100) rendering queue instead of (2950, 3050). The tooltip for priority still seems incorrect as it claims that higher priority values get rendered first, which is the opposite of what happens when increasing the queue. So just a heads up that this screenshot is from a project using 7.4.0 and this might be different for you if you’re on a lower version)

To apply the shadows, you need to use the shader on a quad that will cover the sprite you want shadowed:


Note: You can also place this across the entire screen and it’ll process all sprites on the screen that use the Sprite-Shadowed shader above. It doesn’t necessarily need to be placed on top of a specific sprite.

This approach isn’t ideal in terms of performance, as it requires either a screen-wide shader, or an extra 5-pass shader for every sprite in the scene.
But I guess it’s at least a way to achieve the effect for now, until more proper support is provided officially.

4 Likes

This is how I did it, without using scripts… You’ll need 2 cameras for this and you’ll need to get the Light Weight Render Pipeline. Once you’ve setup your Render pipeline, you can add both renders to the pipeline you’ve created. Next, you’ll need two cameras. Camera “A” will render all of your sprites and Camera “B” will render your 3D assets. Camera “A” will need to be an overlay camera and Camera “B” will need to be your base camera. Once you’ve setup everything up, simply hit the plus button on the camera in the section for “Stack”, then add your overlay camera to it. You should now have both 3D and 2D lights interacting with 3D objects and Sprites.

1 Like

I found a solution that works perfectly.

How To Make 2D Sprite Cast and Receive Shadow in 3D World, Using Unity Shader Graph - Hananon Artworks

1 Like

I can’t get this solutiuon to work with a sprite renderer and the standard 2d animation implimentation using sprite sheets and updating the sprite in the sprite renderer in a unity animation.

Is this how the shader is intended to work or am I missing something?

7448381--913481--upload_2021-8-25_17-38-6.png

I realize this is quite old. But it is exactly what I am looking for! Can you elaborate on how to “setup your render pipeline”?

if that is your question, you need to start by reading unity’s docs related to rendering, and maybe watch some tutorials on using the URP, too. in general, when programming, always scour the docs thoroughly before asking your question. they are your first and most important resource. they will answer all of your basic questions.

in the age of google this is extremely easy. just look up “unity [whatever thing you are confused about]” and find a page whose url starts with “docs.unity3d.com”.

if you’re confused about anything you read, open unity and fuck around with the mechanic until you understand how it works. use the docs as your roadmap. then come to the forums when you’re ready for more. most programming advice in general, including game dev advice, will not be useful until you have that foundational knowledge.

Compared to every reply, this is the most useless.