Spotlight shadows in custom vert/frag shader

Hello everyone,

I am trying to develop a custom shader that receives shadows.

I have tried this solution : Adding Shadows to a Unity Vertex/Fragment Shader in 7 Easy Steps | Alastair Aitchison

It is based on the functions provided in "“AutoLight.cginc” :

LIGHTING_COORDS(0,1) // in vertex output struct

TRANSFER_VERTEX_TO_FRAGMENT(o); // in vertex shader

float attenuation = LIGHT_ATTENUATION(i); // in fragment shader

This solution works great, however my objects only receive the shadows from the main directionnal light.
Is there any way to also get also the shadows from the spotlights and pointlights?

I have tried to add the following line to my shader but the result is the same :

#pragma multi_compile_fwdadd_fullshadows

Moreover, I wonder if there is any way that my objects receive the shadow attenuation value provided by a single light (a spotlight) and not by the other ones?

Thank you.

Unity does its lighting in multiple passes, so if you only have one pass (fwdbase) you only get the directional light & shadows with the other lighting baked into per vertex lighting or the ambient lighting. Vertex lighting and ambient don’t support shadowing, so even if you have that working so you see spot lights you won’t get shadows from then.

Instead you need to add a second pass that uses the multi_compile_fwdadd_fullshadows with some minor adjustments vs the fwdbase pass. I would suggest making a simple surface shader and looking at the generated shader code (select the surface shader, click on “show generated”) and deconstruct what its doing. There will be a ton of passes, forward base, forward add, deferred, prepass, meta, and maybe a shadow caster depending on your surface shader, but only the first two do you need to care about.

2 Likes

I’ve been looking for the same thing for a while now but I don’t have access to generated code. Previously I tried looking in the AutoLight.cginc file for information on forward and differed.
If anyone has found something in the generated code could you start a topic and sticky it so it can be a resource for future users.

You can also download the standard shader code from the Unity website. It perform all the available passes.

Yes but it doesn’t give the generated code from the shader. I’m going to look around some of the include file to see if I can find anything that might give a clue as to how spotlight shadows are called in forward rendering.

There’s generated code for surface shaders, not for the standard shader or vertex fragment shaders in general. There’s compiled shaders, but that doesn’t give you much information either unless you can read shader assembly, though it does give you a look at the keywords the shader is compiled using.

The main thing for understand how spotlights in Unity work is looking at the standard shader or the generated shader code from a very simple surface shader and look at the forward add pass. At the most basic level you just need to make a copy of the forward base pass and change the Tags { “LightMode”=“ForwardBase” } to “ForwardAdd”, though there’s a couple other settings that are needed to get it looking right.

I looked at the diffuse shader generated code with and without fullshadows. There is no difference in the code.
fwdadd_fullshadows must effect something outside of the shader.

It changes the keywords used to compile with.

This is a shader variant’s keyword. Unity - Manual: Declaring and using shader keywords in HLSL

Still that would not explain why when I remove the fullshadows keyword I get the exact same code. the only thing that changes is that I either do or do not have a point light shadow.

Wait, so you saying that instead of looking at the generated code I need to look at the compiled code. -_-

Surface shaders are vertex / fragment shader generators, and vertex / fragment shaders are not the final code used by the GPU to render. The various #pragma multi_compile and #pragma shader_feature lines are both used to tell the shader compiler to make multiple versions of the shader with different features turned on and off (variants). If you look at the shader code you’ll see #if or #ifdef or #ifndef lines, and these are bits of shader code that will be ignored or used based on the keywords.

For example, if you have a shader with

#pragma shader_feature MAKERED

and

fixed4 color = fixed4(1,1,1,1); // white
#ifdef MAKERED
color = fixed4(1,0,0,1); // red
#endif

Unity will compile two versions of the shader, one where the color value is white, and another which overrides it with red, and that can be toggled via code or a material parameter.

So the bit you’re missing is #pragma multi_compile_fwdadd vs multi_compile_fwdadd_fullshadows is the later includes the keywords that enable shadowed lights, whereas the prior does not and lights that have shadows will fallback to the non-shadowed variants of the shader.

1 Like

I’ve looked over the generated code and extracted the first and second forward rendering passes. Next I striped out everything. I am convinced that spotlight and point shadows are some sort of magic because I see nothing in this shader that should still be creating a shadow. I know the magic is in the second pas but I can’t tell where.
Shader code

properties
{
_MainTex("Base (RBG)", 2D) = "white" {}
_Ramp2D("BRDF Skinlights",2D)= "blue"{}


}
SubShader
{

Tags{"RenderType" = "Opaque"}
LOD 200


    // ------------------------------------------------------------
    // Surface shader code generated out of a CGPROGRAM block:
   

    // ---- forward rendering base pass:
    Pass {
        Name "FORWARD"
        Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
// compile directives
#pragma vertex vert_surf
#pragma fragment frag_surf
#pragma target 3.0
#pragma multi_compile_fwdbase
#define UNITY_PASS_FORWARDBASE
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"






struct Input
{
float2 uv_MainTex;
};


// vertex-to-fragment interpolation data
#ifdef LIGHTMAP_OFF
struct v2f_surf {
  float4 pos : SV_POSITION;
  fixed3 normal : TEXCOORD0;
};
#endif
#ifndef LIGHTMAP_OFF
struct v2f_surf {
  float4 pos : SV_POSITION;
};
#endif
#ifndef LIGHTMAP_OFF

#endif

// vertex shader
v2f_surf vert_surf (appdata_full v) {
  v2f_surf o;
  o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
  return o;
}


// fragment shader
fixed4 frag_surf (v2f_surf IN) : SV_Target {
  // prepare and unpack data

  // compute lighting & shadowing factor

  fixed4 c = 0;
  return c;
}

ENDCG

}

    // ---- forward rendering additive lights pass:
    Pass {
        Name "FORWARD"
        Tags { "LightMode" = "ForwardAdd" }
        ZWrite Off Blend OneMinusDstColor One  Fog { Color (0,0,0,0) }

CGPROGRAM
// compile directives
#pragma vertex vert_surf
#pragma fragment frag_surf
#pragma target 3.0
#pragma multi_compile_fwdadd_fullshadows
#define UNITY_PASS_FORWARDADD
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"


sampler2D _MainTex;
sampler2D _Ramp2D;

struct Input
{
float2 uv_MainTex;
};

half4 LightingSkinlights(SurfaceOutput r, half3 lightDir)
{

   
    float4 s;
   
    return s;
   
}

void surf (Input IN, inout SurfaceOutput t)
{
half4 s = float4(.5, .5,.5, 1); 
t.Albedo = s.rgb;
t.Alpha = s.a;


}





// vertex-to-fragment interpolation data
struct v2f_surf {
  float4 pos : SV_POSITION;
  fixed3 normal : TEXCOORD0;
  half3 lightDir : TEXCOORD1;
  //SHADOW_COORDS(4) // for shadows wont work
  LIGHTING_COORDS(2,3)
};

// vertex shader
v2f_surf vert_surf (appdata_full v) {
  v2f_surf o;
  o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
  o.normal = mul((float3x3)_Object2World, SCALED_NORMAL);
  float3 lightDir = WorldSpaceLightDir( v.vertex );
  o.lightDir = lightDir;

  // pass lighting information to pixel shader
  TRANSFER_VERTEX_TO_FRAGMENT(o);
  TRANSFER_SHADOW(o);//for shadows
  return o;
}

// fragment shader
fixed4 frag_surf (v2f_surf IN) : SV_Target {
  // prepare and unpack data
  #ifdef UNITY_COMPILER_HLSL

  #else
  Input surfIN;
  #endif
  #ifdef UNITY_COMPILER_HLSL
  SurfaceOutput o = (SurfaceOutput)0;
  #else
  SurfaceOutput o;

  o.Normal = IN.normal;

  // call surface function
  surf (surfIN, o);
  #ifndef USING_DIRECTIONAL_LIGHT
  fixed3 lightDir = normalize(IN.lightDir);
  #else
  fixed3 lightDir = IN.lightDir;
  #endif
  fixed4 c = LightingLambert (o, lightDir, SHADOW_ATTENUATION(IN));//LIGHT_ATTENUATION(IN));
  c.a = 0.0;
  return c;
}

ENDCG

}
}


Fallback "Diffuse"



}

I switched LIGHT_ATTENUATION with SHADOW_ATTENUATION and added TRANSFER_SHADOW(o) and it still worked

You mean apart from the SHADOW_ATTENUATION macro, which is rather explicitly named as something that might be related to shadows?

Look at AutoLight.cginc and look at what SHADOW_ATTENUATION and LIGHT_ATTENUATION do.

Just making things clear

  1. This shader was produced from the generated code of a surface shader; then gutted. What left of the code is necessary to keep the shader from being pink. There isn’t even a functional lighting function.

  2. I just wrote that I was the one that switched it from LIGHT_ATTENUATION to SHADOW_ATTENUATION and there was no change in the shader or shadows. I’ve already been through AutoLight.cginc, that one of the things I mentioned earlier.

  3. There is nothing I see in this shader that isn’t in ever other fragment shader that supports directional shadows. Yet this shader supports point light shadows too. For example autolight, unitycg, lighting cords, light or shadow attenuation.

  4. when searching the net, there is no indication that anyone has found a way to get point light or spot light shadows int a fragment shader. So if it was so easy as just looking at the includes it would have been done by now.

But there is a lighting function. In the Foward Add pass, which is used to render additional lights like spot lights and point lights, you’re calling LightingLambert() which is the lighting function used by the built in diffuse shader.

  1. Because the light attenuatuon function in some cases just calls the shadow attenuatuon macro and does nothing else. When it comes to shadows, they’re effectively the same function. If you actually look at the AutoLight.cginc you’d see that.

  2. Except for this shader has the forward add pass, where most basic shaders out there do not.

  3. What you posted is just a vertex fragment shader. It just happens to be one generated by a surface shader and is therefore a bit messy. I would guess a vast number of the popular shaders on the asset store are vertex fragment shaders and support point and spot shadows, like Uber Standard, or RTP, or anything made using Shader Forge. I almost never use surface shaders myself because I’m usually looking for more control than is possible with a surface shader, though I have on occasion started with a surface shader’s generated code like you posted above.

One last thing is that shader has the line Fallback “Diffuse” which if almost everything else in your shader is removed would mean it would continue to work since that shader has support for directional lighting and point / spot lights and your shader would use the passes from that shader if they were missing from yours. That’s why your shader casts shadows currently even though you don’t have a shadow caster pass.

  1. The problem is you can’t just take thing out of AutoLight.cginc and get them to work in your shader. So it’s a mystery what the surface shader is really calling on.

  2. I can except this but for my shader I have a forward add pass too.

  3. I have gutted the first and second pass surface shader but Fallback “Diffuse” is not being called unless a shader can call Fallback while still using what’s left of the gutted code. So that leaves the last option that even though there was a lighting function in the surface shader originally it was phoning in another lighting function all this time anyway?

I can only suspect that nothing in that shader is using #pragma multi_compile_fwdadd_fullshadows and is instead passing this keyword to the LightingLambert model found in Lighting.cginc. But no I still do not know how the spotlight and point light shadows are accessed.
I think Lighting.cginchas something to do with it because it’s an include that i don’t see as much.