Unity Rendering Principle (14) Unity's Light Source

A world with only one parallel light is nice, but we really just need to deal with more complex light types and a larger number of light sources in Unity Shader.

Unity supports a total of 4 light sources, parallel light, point light, spotlight and surface light. Surface light is only useful when baking.

Due to the different geometric definitions of each light source, their corresponding light source properties are also different, which requires us to treat them differently. Fortunately, Unity provides us with many functions to handle them.

What is the impact of the type of light source

The type of light source will have an impact on the Shader. We can consider which properties of the light source are used by the Shader. The most commonly used are the position, direction, color, intensity and attenuation of the light source, and these five properties are closely related to their geometric information.

Parallel light

The geometric definition of parallel light is the simplest, and its illumination range is unlimited, usually appearing in the scene as a character such as the sun.

The reason why parallel light is simple is that it does not have a unique position, that is to say, it can be placed anywhere in the scene, and its geometric properties are only directions. We can adjust the Rotation property of the Transform of the parallel light to change it. The direction of the light source, and the direction of the parallel light to all points in the scene is the same. In addition, since the parallel light does not have a specific location, there is no concept of attenuation, that is, the light intensity does not change with distance.

Point light source

The point light source illuminates a limited space, which is defined by a sphere in space. A point light source can represent the light emitted by a point and extending in all directions, that is, its illumination range is a sphere.

  • The radius of the sphere can be adjusted by the Range property of the panel
  • Its location information can also be modified by the Transform component
  • Direction property, we need to subtract the position of a point from the position of the point light source to get the direction of the point
  • The color and intensity of the light source can be adjusted in the Light component
  • At the same time, the point light source will attenuate. As the object gradually moves away from the point light source, the light intensity it receives will gradually decrease. The light intensity at the center of the sphere of the point light source is the strongest, and the boundary is the weakest. The attenuation value in the middle can be calculated by function.

Spotlight

Its illuminated space is limited, but it is no longer a simple sphere, but a cone. Its property calculation method is generally similar to that of the point light source, but the attenuation calculation function of the light source needs to determine whether it is within the range of the vertebral body.

Handling different types of light sources in forward rendering

After understanding the geometric definition of light sources in 3, let’s take a look at how to access their five properties in Unity Shader, position, direction, color, intensity, and attenuation

Shader

(1) First, we need to define the first Pass - Base Pass. For this, we need to set the label of the render path of the Pass

1
2
3
4
5
6
Pass {
Tags{"LightMode"="ForwardBase"}

CGPROGRAM
#pragma mulit_compile_fwdbase
}

In addition to setting the render path, we also used the’prgama 'compile directive, which ensures that we can use lighting attenuation and other care variables in the Shader to be correctly assigned, which is indispensable.

(2) In the slice element shader of Base Pass, we first calculate the ambient light in the scene

1
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

We want the ambient light to be calculated once, so this part will not be calculated in the Additional Pass later. Similarly, there is a self-luminous object, but we do not consider self-luminous for the time being

(3) Then, we processed the most important parallel light in the scene in Base Pass. In this example, there is only one parallel light in the scene. If there are multiple parallel lights, Unity will select the brightest parallel light and pass it to Base Pass for pixel-by-pixel processing, and other parallel lights will be processed vertex-by-vertex or pixel-by-pixel in Additional Pass. If there are no parallel lights in the scene, Base Pass will be treated as an all-black light source.

We mentioned that each light source has 5 properties, position, direction, color, intensity and attenuation. For Base Pass, the pixel-by-pixel light source type it handles must be parallel light, we can use _WorldSpaceLightPos0 to get the direction of this parallel light (position does not make sense for parallel light), use _LightColor0 to get its color and intensity (_LightColor0 is already the result of multiplying color and intensity), since parallel light can be considered unattenuated, so we directly make the attenuation value 1

1
2
3
4
5
6
7
8
9
10
11
12
13
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);

fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

fixed atten = 1.0;

return fixed4(ambient + (diffuse + specular) * atten, 1.0);

At this point, the work of Base Pass is complete

(4) Next, we need to define Additional Pass for other pixel-by-pixel light sources in the scene. For this, we prefer to set the render path label of Pass

1
2
3
4
5
6
Pass {
Tags{"LightMode"="ForwardAdd"}
Blend One One
CGPROGRAM
#pragma multi_compile_fwdadd
}

In addition to setting the render path, we also used the #pragma multi_compile_fwdadd command to ensure that we can access the correct lighting variables in Additional Pass.

Unlike Base Pass, we also enable the Blend command and set the Blend Mode, because we want the lighting results calculated by the Additional Pass to be superimposed on the previous lighting results in the frame cache. If the Blend command is not used, the Additional Pass will directly overwrite the previous lighting results. The blending factor we are using now is’Blend One One ', it is not necessary, we can set it to any coefficient supported by Unity

(5) Generally speaking, the lighting processing of Additional Pass is the same as that of Base Pass, so we only need to paste the code of Base Pass into Additonal Pass and modify it slightly. These modifications are often to remove the ambient light in Base Pass, self-luminous, point-by-point lighting, SH lighting, etc., and add support for some different light sources.

Therefore, we did not calculate the ambient light in the scene. Since the type of light source processed by Additional Pass may be parallel light, point light or spotlight, we can still use _LightColor0 to calculate the 5 properties of the light source - position, direction, color, intensity and attenuation, but for properties such as position, direction and attenuation, we need to calculate them separately according to the type of light source.

First, let’s take a look at how to calculate the direction of different light sources

1
2
3
4
5
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPosition.xyz);
#endif

The above code first determines the type of pixel-by-pixel light source currently processed, which is obtained by using the ‘#ifdef’ command to determine whether USING_DIRECTIONAL_LIGHT ‘is defined. If the light type processed by the current forward rendering Pass is parallel light, Unity’s underlying rendering engine will define USING_DIRECTIONAL_LIGHT. If it is determined to be parallel light, the light source direction can be obtained directly from the’ _ WorldSpaceLightPos0.xyz ‘. If it is a point light source or spotlight, then the’ _ WorldSpaceLigthPos0.xyz 'represents the position of the light source in world space. To get the direction of the light source, you need to subtract the position of the vertices in world space from this position

Finally, we need to deal with the attenuation of different light sources

1
2
3
4
5
6
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPosition, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, LightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif

We also use USING_DIRECTIONAL_LIGHT to determine whether it is parallel light, and the attenuation value of non-parallel light. Although mathematical expressions can be used to calculate the attenuation of a given point relative to the point light source and spotlight, these calculations often involve relatively large calculations such as square root, division, etc. Large operations, so Unity chooses to use a texture as a Look up table (Lookup Table, LUT) to obtain the attenuation of the light source in the chip element shader. We first obtain the coordinates in the light source space, and then use the coordinates to sample the attenuation texture to obtain the attenuation value

The final code is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
Shader "Unlit/ForwardRendering"
{
Properties
{
_Diffuse("Diffuse", Color) = (1,1,1,1)
_Specular("Specular", Color) = (1,1,1,1)
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma multi_compile_fwdbase
#include "Lighting.cginc"

#pragma vertex vert
#pragma fragment frag

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

struct a2v
{
float4 vertex: POSITION;
float3 normal: NORMAL;
};

struct v2f
{
float4 pos: SV_POSITION;
float3 worldNormal: TEXCOORD0;
float3 worldPos : TEXCOORD1;
};

v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);

fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

fixed atten = 1.0;

return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}

ENDCG
}
Pass
{
Tags{"LightMode" = "ForwardAdd"}
Blend One One
CGPROGRAM
#pragma multi_compile_fwdbase
#include "Lighting.cginc"

#pragma vertex vert
#pragma fragment frag

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

struct a2v
{
float4 vertex: POSITION;
float3 normal: NORMAL;
};

struct v2f
{
float4 pos: SV_POSITION;
float3 worldNormal: TEXCOORD0;
float3 worldPos : TEXCOORD1;
};

v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif

fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);

fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPosition, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, LightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif

return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}

ENDCG
}
}

Fallback "Diffuse"
}

The above code only applies to the Standard pipeline, for URP pipeline is no longer applicable, the specific transformation method can refer to this blog: https://zhuanlan.zhihu.com/p/336428407

Base

We create a parallel light and 4 point light sources in the scene

When we create a light source, its Render Mode is Auto by default, which means Unity will determine for us which light sources will be processed pixel by pixel and which will be processed point by point or SH. Since we didn’t change the value in’Piexl Light Color ', by default an object can accept the 4 brightest pixel-by-pixel lights other than parallel lights. In this example, there are 5 light sources in the scene, one of which is Parallel light, it will be processed pixel-by-pixel in Base Pass, and the other 4 point lights will be processed pixel-by-pixel in Additional Pass.

Unity processes these light sources in order of importance. All point light sources in our scene have the same color and intensity, so their importance depends on how close they are to the capsule.

For an object in the scene, if it is not within the influence range of the light source, Unity will not call Pass for this object.

If there are many pixel-by-pixel light sources in the scene, the Additonal Pass of the object will be called multiple times, which will affect performance, so we can set the Render Mode of the light source to “Not Important”, so that Unity will not treat the light source as pixel-by-pixel.

In our example, the vertex shader does not handle the light source, so if you set 4 point light sources to be “Not Important”, the result is that these 4 light sources will not have an effect.