US3S5L6——多种光源综合实现

知识回顾

  1. 前向渲染路径中如何处理光源

    两个Pass

    • Base Pass(基础渲染通道)
    • Additional Pass(附加渲染通道)
  2. 在 Shader 当中如何判断多种光源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #ifdef USING_DIRECTIONAL_LIGHT​
    // 平行光逻辑
    #else
    #if defined(POINT)
    // 点光源逻辑
    #elif defined(SPOT)
    // 聚光灯逻辑
    #else
    // 其他逻辑
    #endif
    #endif
  3. 点光源衰减值计算

    1
    2
    float3 lightCoord = mul(unity_WorldToLight, float4(worldPos, 1)).xyz;                        // 计算光源坐标
    fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).xx).UNITY_ATTEN_CHANNEL; // 从光照衰减纹理内取出衰减值
  4. 聚光灯衰减值计算

    1
    2
    3
    4
    float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));                // 计算光源坐标
    fixed atten = (lightCoord.z > 0) * // 第一步,通过z分量判断目标点是否在聚光灯背面
    tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * // 第二步,将坐标进行变换后从cookie纹理内取出遮罩衰减值
    tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL; // 第三步,根据坐标从光照衰减纹理内取出距离衰减值

前向渲染路径中处理多种光源的综合实现

主要步骤:

  1. 新建一个 Shader 文件,删除其中无用代码

  2. 复用之前的 Blinn-Phong 光照模型的逐片元光照

    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
    Shader "TeachShader/Lesson64"
    {
    Properties
    {
    _MainColor("MainColor", Color) = (1, 1, 1, 1) //材质的漫反射颜色
    _SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) //材质高光反射颜色
    _SpecularNum("SpecularNum", Range(0, 20)) = 0.5 //光泽度
    }
    SubShader
    {
    Pass
    {
    Tags { "LightMode" = "ForwardBase" }

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"
    #include "Lighting.cginc"

    struct v2f
    {
    float4 svPos : SV_POSITION; //裁剪空间下的顶点坐标
    float3 wNormal : NORMAL; //世界空间下的法线
    float3 wPos : TEXCOORD0; //世界空间下的顶点坐标
    };

    fixed4 _MainColor; //属性设置的漫反射颜色
    fixed4 _SpecularColor; //属性设置的材质高光颜色
    float _SpecularNum; //属性设置的光泽度

    //计算兰伯特光照模型 颜色相关函数(逐片元)
    //参数:
    // wNormal: 世界空间下顶点的法线信息
    fixed3 getFragLambertColor(in float3 wNormal)
    {
    float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); //将光源0的位置标准化,得到方向,用于计算夹角
    //兰伯特光照模型的实现,这里的颜色计算只取rgb,不考虑透明度的情况
    fixed3 color = _LightColor0.rgb * _MainColor.rgb * max(0, dot(wNormal, lightDir));
    return color;
    }

    //计算Blinn-Phong高光反射光照模型 颜色相关函数(逐片元)
    //参数:
    // wPos: 世界空间下顶点坐标
    // wNormal: 世界空间下顶点的法线信息
    fixed3 getFragSpecularColor(in float3 wPos, in float3 wNormal)
    {
    float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - wPos); //计算观察方向
    float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); //标准化光源方向
    float3 halfA = normalize(viewDir + lightDir); //将光源方向和观察方向相加得到其半角向量,并标准化
    //Blinn-Phong高光反射模型的实现,这里的颜色计算只取rgb,不考虑透明度的情况
    fixed3 color = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(wNormal, halfA)), _SpecularNum);
    return color;
    }

    v2f vert (appdata_base v)
    {
    v2f v2fData;
    v2fData.svPos = UnityObjectToClipPos(v.vertex); //顶点转换到裁剪空间
    v2fData.wNormal = UnityObjectToWorldNormal(v.normal); //法线转换到世界空间
    v2fData.wPos = mul(unity_ObjectToWorld, v.vertex).xyz; //顶点转换到世界空间
    return v2fData;
    }

    fixed4 frag (v2f i) : SV_Target
    {
    //计算Blinn-Phong式光照模型需要的各种颜色
    fixed3 lambertColor = getFragLambertColor(i.wNormal); //计算漫反射
    fixed3 specularColor = getFragSpecularColor(i.wPos, i.wNormal); //计算高光反射颜色
    fixed3 blinnPhongColor = UNITY_LIGHTMODEL_AMBIENT.rgb + lambertColor + specularColor; //计算Blinn-Phong式光照模型颜色

    return fixed4(blinnPhongColor.rgb, 1); //因为传递过来的颜色变量不包括透明度,因此这里需要手动指定透明度
    }
    ENDCG
    }
    }
    }
  3. 其中已存在的 Pass​,即 Base Pass(基础渲染通道)

    我们需要为它加上一个编译指令 #pragma multi_compile_fwdbase
    该指令可以保证我们在 Shader 中使用光照衰减等光照等变量可以被正确赋值,并且会帮助我们编译 Base Pass 中所有变体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // Bass Pass 基础渲染通道
    Pass
    {
    Tags { "LightMode" = "ForwardBase" }

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    // 帮助我们编译所有光照变体,并确保光照衰减相关的变量能够正确复制到对应的变量中
    #pragma multi_compile_fwdbase

    #include "UnityCG.cginc"
    #include "Lighting.cginc"
    // ...
    ENDCG
    }
  4. 复制 Base Pass,基于它来修改我们的 Additional Pass(附加渲染通道)

  5. LightMode​ 改为 ForwardAdd

  6. 加入混合命令 Blend One One​ 表示开启混合 线性减淡效果

  7. 加入编译指令 #pragma multi_compile_fwdadd

    该指令保证我们在附加渲染通道中能访问到正确的光照变量,并且会帮助我们编译 Additional Pass 中所有变体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // Additional Pass 附加渲染通道
    Pass
    {
    Tags { "LightMode" = "ForwardAdd" }
    // 使用线性减淡效果进行光照混合
    Blend One One

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    // 帮助我们编译所有光照变体,并确保光照衰减相关的变量能够正确复制到对应的变量中
    #pragma multi_compile_fwdadd

    #include "UnityCG.cginc"
    #include "Lighting.cginc"
    // ...
    ENDCG
    }
  8. 修改相关代码,基于不同的光照类型来计算衰减值

    1. 光的方向计算方向修改
    2. 基于不同光照类型计算衰减值
    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
    fixed4 frag (v2f i) : SV_Target
    {
    // 漫反射颜色的计算
    fixed3 worldNormal = normalize(i.wNormal);
    // 光的方向
    #if defined(_DIRECTIONAL_LIGHT)
    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); // 平行光 光的方向就是它的位置
    #else
    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.wPos); // 点光源和聚光灯 光的方向 是 光的位置 - 顶点位置
    #endif
    fixed3 diffuse = _LightColor0.rgb * _MainColor.rgb * max(0, dot(worldNormal, worldLightDir));

    // 高光反射颜色的计算
    fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.wPos.xyz); // 视角方向
    fixed3 halfDir = normalize(worldLightDir + viewDir); // 半角向量
    fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfDir)), _SpecularNum);

    // 衰减值的计算
    #ifdef USING_DIRECTIONAL_LIGHT //如果光源是平行光,Unity就会定义USING_DIRECTIONAL_LIGHT这个宏,因此会进入这段逻辑
    // 平行光逻辑
    fixed atten = 1;
    #else
    // 如果未定义USING_DIRECTIONAL_LIGHT,则说明此光源非平行光
    #if defined(POINT)
    // 点光源逻辑
    float3 lightCoord = mul(unity_WorldToLight, float4(i.wPos, 1)).xyz;
    fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).xx).UNITY_ATTEN_CHANNEL;
    #elif defined(SPOT)
    // 聚光灯逻辑
    float4 lightCoord = mul(unity_WorldToLight, float4(i.wPos, 1));
    fixed atten = (lightCoord.z > 0) *
    tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w *
    tex2D(_LightTextureB0, dot(lightCoord, lightCoord).xx).UNITY_ATTEN_CHANNEL;
    #else
    // 其他逻辑
    fixed atten = 1;
    #endif
    #endif

    // 附加渲染通道内不需要再加上环境光颜色了,因为它只需要计算一次,而之前已经在基础渲染通道中计算了
    fixed3 blinnPhongColor = (diffuse + specular) * atten;
    return fixed4(blinnPhongColor.rgb, 1);
    }

完整 Shader 代码如下:

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
Shader "TeachShader/Lesson64"
{
Properties
{
_MainColor("MainColor", Color) = (1, 1, 1, 1) //材质的漫反射颜色
_SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) //材质高光反射颜色
_SpecularNum("SpecularNum", Range(0, 20)) = 0.5 //光泽度
}
SubShader
{
// Bass Pass 基础渲染通道
Pass
{
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 帮助我们编译所有光照变体,并确保光照衰减相关的变量能够正确复制到对应的变量中
#pragma multi_compile_fwdbase

#include "UnityCG.cginc"
#include "Lighting.cginc"

struct v2f
{
float4 svPos : SV_POSITION; //裁剪空间下的顶点坐标
float3 wNormal : NORMAL; //世界空间下的法线
float3 wPos : TEXCOORD0; //世界空间下的顶点坐标
};

fixed4 _MainColor; //属性设置的漫反射颜色
fixed4 _SpecularColor; //属性设置的材质高光颜色
float _SpecularNum; //属性设置的光泽度

//计算兰伯特光照模型 颜色相关函数(逐片元)
//参数:
// wNormal: 世界空间下顶点的法线信息
fixed3 getFragLambertColor(in float3 wNormal)
{
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); //将光源0的位置标准化,得到方向,用于计算夹角
//兰伯特光照模型的实现,这里的颜色计算只取rgb,不考虑透明度的情况
fixed3 color = _LightColor0.rgb * _MainColor.rgb * max(0, dot(wNormal, lightDir));
return color;
}

//计算Blinn-Phong高光反射光照模型 颜色相关函数(逐片元)
//参数:
// wPos: 世界空间下顶点坐标
// wNormal: 世界空间下顶点的法线信息
fixed3 getFragSpecularColor(in float3 wPos, in float3 wNormal)
{
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - wPos); //计算观察方向
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); //标准化光源方向
float3 halfA = normalize(viewDir + lightDir); //将光源方向和观察方向相加得到其半角向量,并标准化
//Blinn-Phong高光反射模型的实现,这里的颜色计算只取rgb,不考虑透明度的情况
fixed3 color = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(wNormal, halfA)), _SpecularNum);
return color;
}

v2f vert (appdata_base v)
{
v2f v2fData;
v2fData.svPos = UnityObjectToClipPos(v.vertex); //顶点转换到裁剪空间
v2fData.wNormal = UnityObjectToWorldNormal(v.normal); //法线转换到世界空间
v2fData.wPos = mul(unity_ObjectToWorld, v.vertex).xyz; //顶点转换到世界空间
return v2fData;
}

fixed4 frag (v2f i) : SV_Target
{
//计算Blinn-Phong式光照模型需要的各种颜色
fixed3 lambertColor = getFragLambertColor(i.wNormal); //计算漫反射
fixed3 specularColor = getFragSpecularColor(i.wPos, i.wNormal); //计算高光反射颜色
fixed atten = 1; //衰减值,由于Bass Pass只处理平行光,因此衰减值默认为1
//计算Blinn-Phong式光照模型颜色,衰减值 需要乘以 漫反射颜色 和 高光反射颜色 的和
fixed3 blinnPhongColor = UNITY_LIGHTMODEL_AMBIENT.rgb + (lambertColor + specularColor) * atten;

return fixed4(blinnPhongColor.rgb, 1); //因为传递过来的颜色变量不包括透明度,因此这里需要手动指定透明度
}
ENDCG
}

// Additional Pass 附加渲染通道
Pass
{
Tags { "LightMode" = "ForwardAdd" }
// 使用线性减淡效果进行光照混合
Blend One One

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 帮助我们编译所有光照变体,并确保光照衰减相关的变量能够正确复制到对应的变量中
#pragma multi_compile_fwdadd

#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"

struct v2f
{
float4 svPos : SV_POSITION; //裁剪空间下的顶点坐标
float3 wNormal : NORMAL; //世界空间下的法线
float3 wPos : TEXCOORD0; //世界空间下的顶点坐标
};

fixed4 _MainColor; //属性设置的漫反射颜色
fixed4 _SpecularColor; //属性设置的材质高光颜色
float _SpecularNum; //属性设置的光泽度

v2f vert (appdata_base v)
{
v2f v2fData;
v2fData.svPos = UnityObjectToClipPos(v.vertex); //顶点转换到裁剪空间
v2fData.wNormal = UnityObjectToWorldNormal(v.normal); //法线转换到世界空间
v2fData.wPos = mul(unity_ObjectToWorld, v.vertex).xyz; //顶点转换到世界空间
return v2fData;
}

fixed4 frag (v2f i) : SV_Target
{
// 漫反射颜色的计算
fixed3 worldNormal = normalize(i.wNormal);
// 光的方向
#if defined(_DIRECTIONAL_LIGHT)
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); // 平行光 光的方向就是它的位置
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.wPos); // 点光源和聚光灯 光的方向 是 光的位置 - 顶点位置
#endif
fixed3 diffuse = _LightColor0.rgb * _MainColor.rgb * max(0, dot(worldNormal, worldLightDir));

// 高光反射颜色的计算
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.wPos.xyz); // 视角方向
fixed3 halfDir = normalize(worldLightDir + viewDir); // 半角向量
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfDir)), _SpecularNum);

// 衰减值的计算
#ifdef USING_DIRECTIONAL_LIGHT //如果光源是平行光,Unity就会定义USING_DIRECTIONAL_LIGHT这个宏,因此会进入这段逻辑
// 平行光逻辑
fixed atten = 1;
#else
// 如果未定义USING_DIRECTIONAL_LIGHT,则说明此光源非平行光
#if defined(POINT)
// 点光源逻辑
float3 lightCoord = mul(unity_WorldToLight, float4(i.wPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).xx).UNITY_ATTEN_CHANNEL;
#elif defined(SPOT)
// 聚光灯逻辑
float4 lightCoord = mul(unity_WorldToLight, float4(i.wPos, 1));
fixed atten = (lightCoord.z > 0) *
tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w *
tex2D(_LightTextureB0, dot(lightCoord, lightCoord).xx).UNITY_ATTEN_CHANNEL;
#else
// 其他逻辑
fixed atten = 1;
#endif
#endif

// 附加渲染通道内不需要再加上环境光颜色了,因为它只需要计算一次,而之前已经在基础渲染通道中计算了
fixed3 blinnPhongColor = (diffuse + specular) * atten;
return fixed4(blinnPhongColor.rgb, 1);
}
ENDCG
}
}
}

显示效果(漫反射颜色改为灰色,且光泽度调到20):

image

可以看到,这样的 Shader 可以受到多种光源的影响,并反射出正确的颜色