Attenuation and shadows for pixel lights
Reference Manual > ShaderLab Reference > Advanced ShaderLab topics > Unity's Rendering Pipeline > Attenuation and shadows for pixel lightsThe different light types in Unity's pixel lighting pipeline are implemented using texture lookups to do the attenuation:
- Directional lights can't attenuate, so they don't need any extra processing.
- If a cookie is set up, a directional light does one texture lookup into the cookie.
- Point lights don't need extra processing if attenuation is turned off. If attenuation is used, a texture lookup needs to be done. This is done by doing a single volume (3D) texture lookup; or by doing two (one 2D and one 1D) lookups if the graphics card does not support volume textures.
- If a cookie is set up, attenuation is always turned off and the light does one texture lookup into the cookie cubemap.
- Spot lights use two texture lookups: one to get the spotlight shape and another to do distance attenuation.
- If a cookie is set up, it is used to get spotlight shape. The light still uses two texture lookups.
Additionally, if a pixel light casts shadows, and the object receives shadows, then the shadow map is looked up one or more times:
- Spot lights use a single shadow map.
- Point lights use a single shadow cubemap, possibly with RGBA encoding of the depth.
- Directional lights use one or more shadow maps ("cascaded shadow maps").
Supporting all these combinations can quickly get out of hand! For this reason, Unity uses a multi compilation scheme, where shader author has to use several macros to compute the light attenuation & shadows, and all the needed permutations will be generated by Unity.
Attenuation in fragment programs
When using fragment programs, Unity helps setting these combinations up by providing some macros and definitions in AutoLight.cginc and UnityCG.cginc Cg include files (for GLSL: AutoLight.glslinc and UnityCG.glslinc). Then what you do is:
- Use ``#pragma multi_compile_builtin'' directive at start of your program snippet (CGPROGRAM / GLSLPROGRAM block)
- Include the helper files:
- For Cg: #include "AutoLight.cginc" and #include "UnityCG.cginc"
- For GLSL: #include "AutoLight.glslinc" and #include "UnityCG.glslinc"
- Add some definitions to the structure that is passed from vertex to fragment program: add LIGHTING_COORDS macro before any texture interpolators. Additionally, don't use semantics on texture interpolators; leave them to be assigned automatically.
- Use TRANSFER_VERTEX_TO_FRAGMENT(o); macro at the end of your vertex program. Here o is the variable that holds the output structure.
- Use LIGHT_ATTENUATION(i) macro in the fragment program to compute attenuation & shadows. Here i is the variable that holds the input structure. The light attenuation macro returns a single float value that combines light attenuation, cookie and shadow - the "illuminance".
This is pretty complex, so a full shader example is in order. This shader fully shows how to use compute light attenuation and shadows in a fragment program. The rest of the shader is kept minimal - it exposes just a color and computes a simple diffuse lighting per-vertex.
Shader "Light Attenuation" {
Properties {
_Color ("Main Color", Color) = (1,1,1,0.5)
}
Category {
Blend AppSrcAdd AppDstAdd
Fog { Color [_AddFog] }
// Fragment program cards
SubShader {
// Ambient pass
Pass {
Tags {"LightMode" = "PixelOrNone"}
Color [_PPLAmbient]
SetTexture [_Dummy] {constantColor [_Color] Combine primary DOUBLE, constant}
}
// Vertex lights
Pass {
Tags {"LightMode" = "Vertex"}
Lighting On
Material {
Diffuse [_Color]
Emission [_PPLAmbient]
}
SetTexture [_Dummy] {constantColor [_Color] Combine primary DOUBLE, constant}
}
// Pixel lights
Pass {
Tags { "LightMode" = "Pixel" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_builtin
#pragma fragmentoption ARB_fog_exp2
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
#include "AutoLight.cginc"
// Define the structure
struct v2f {
V2F_POS_FOG;
LIGHTING_COORDS // <= note no semicolon!
float4 color : COLOR0;
};
// Vertex program
v2f vert (appdata_base v)
{
v2f o;
PositionFog( v.vertex, o.pos, o.fog );
// compute a simple diffuse per-vertex
float3 ldir = normalize( ObjSpaceLightDir( v.vertex ) );
float diffuse = dot( v.normal, ldir );
o.color = diffuse * _ModelLightColor0;
// compute&pass data for attenuation/shadows
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}
// Fragment program
float4 frag (v2f i) : COLOR
{
// Just multiply interpolated color with attenuation
return i.color * LIGHT_ATTENUATION(i) * 2;
}
ENDCG
}
}
}
Fallback "VertexLit"
}
Given such a shader, Unity will compile dozens of combinations required to support different light types, with or without cookies and with or without shadows. It will also compile to OpenGL programs and Direct3D 9 shaders. All you have to do is use these light attenuation macros, and the dirty details will be handled for you!
Attenuation for older hardware
Writing pixel lit shaders for older hardware (that does not support fragment programs) is more involved due to resource constraints and the fact that these shaders have to be written in an assembly-like language. In this case shadows are not supported (again, because of hardware restrictions). Most often you end up writing separate passes that support 0, 1 or 2 light attenuation textures.
For example, the shader above written for ATI Fragment Shader cards (Radeon 8500 and up) would be like the example below. Ambient and Vertex passes are still the same in this case, but there are separate Pixel passes for 0, 1 and 2 attenuation textures, with different shader programs accordingly. In a real shader you'd implement both SubShaders in a single shader, and possibly a couple more for even older video cards (gotta love all the different cards out there, right?).
Shader "Light Attenuation" {
Properties {
_Color ("Main Color", Color) = (1,1,1,0.5)
}
CGINCLUDE
// This block will be pasted into all later Cg program blocks
#include "UnityCG.cginc"
float4 Lighting( appdata_base v )
{
// compute a simple diffuse per-vertex
float3 ldir = normalize( ObjSpaceLightDir( v.vertex ) );
float diffuse = dot( v.normal, ldir );
return diffuse * _ModelLightColor0 * 2;
}
ENDCG
Category {
Blend AppSrcAdd AppDstAdd
Fog { Color [_AddFog] }
// ATI Fragment shader cards
SubShader {
// Ambient pass
Pass {
Name "BASE"
Tags {"LightMode" = "PixelOrNone"}
Color [_PPLAmbient]
SetTexture [_Dummy] {constantColor [_Color] Combine primary DOUBLE, constant}
}
// Vertex lights
Pass {
Tags {"LightMode" = "Vertex"}
Lighting On
Material {
Diffuse [_Color]
Emission [_PPLAmbient]
}
SetTexture [_Dummy] {constantColor [_Color] Combine primary DOUBLE, constant}
}
// Lights with 0 light textures
Pass {
Tags {
"LightMode" = "Pixel"
"LightTexCount" = "0"
}
CGPROGRAM
#pragma vertex vert
struct v2f {
V2F_POS_FOG;
float4 color : COLOR0;
};
v2f vert (appdata_base v)
{
v2f o;
PositionFog( v.vertex, o.pos, o.fog );
o.color = Lighting( v );
return o;
}
ENDCG
Program "" {
SubProgram {
"!!ATIfs1.0
StartOutputPass;
MOV r0, color0; # just output color
EndPass;
"
}
}
}
// Lights with 1 light texture
Pass {
Tags {
"LightMode" = "Pixel"
"LightTexCount" = "1"
}
CGPROGRAM
#pragma vertex vert
uniform float4x4 _SpotlightProjectionMatrix0;
struct v2f {
V2F_POS_FOG;
float4 color : COLOR0;
float4 LightCoord0 : TEXCOORD0; // one light texcoord
};
v2f vert (appdata_base v)
{
v2f o;
PositionFog( v.vertex, o.pos, o.fog );
o.color = Lighting( v );
o.LightCoord0 = mul(_SpotlightProjectionMatrix0, v.vertex); // light texcoord
return o;
}
ENDCG
Program "" {
SubProgram {
"!!ATIfs1.0
StartOutputPass;
SampleMap r0, t0.str; # attenuation
MUL r0, color0, r0.a; # multiply with color
EndPass;
"
}
}
SetTexture[_LightTexture0] {}
}
// Lights with 2 light textures
Pass {
Tags {
"LightMode" = "Pixel"
"LightTexCount" = "2"
}
CGPROGRAM
#pragma vertex vert
uniform float4x4 _SpotlightProjectionMatrix0;
uniform float4x4 _SpotlightProjectionMatrixB0;
struct v2f {
V2F_POS_FOG;
float4 color : COLOR0;
float4 LightCoord0 : TEXCOORD0; // two light texcoords
float4 LightCoordB0 : TEXCOORD1;
};
v2f vert (appdata_base v)
{
v2f o;
PositionFog( v.vertex, o.pos, o.fog );
o.color = Lighting( v );
o.LightCoord0 = mul(_SpotlightProjectionMatrix0, v.vertex);
o.LightCoordB0 = mul(_SpotlightProjectionMatrixB0, v.vertex);
return o;
}
ENDCG
Program "" {
SubProgram {
"!!ATIfs1.0
StartOutputPass;
SampleMap r0, t0.stq_dq; # attenuation1
SampleMap r1, t1.stq_dq; # attenuation2
MUL r0, color0, r0.a;
MUL r0, r0, r1.a;
EndPass;
"
}
}
SetTexture[_LightTexture0] {}
SetTexture[_LightTextureB0] {}
}
}
}
Fallback "VertexLit"
}