US3S7L2——标准高光反射Shader

标准高光反射Shader

和 标准漫反射Shader 一样,所谓的标准高光反射Shader,其实就是一个常用的高光反射 Shader 而已

该 Shader 其实就是一个带有法线(世界空间中计算-全局效果的表现更准确)的基于 BlinnPhong 光照模型的支持多光源和阴影的 Shader

制作常用高光反射Shader

  1. 新建一个 Shader,取名叫 BumpedSpecular​(凹凸镜面反射)

  2. 复用 世界空间下计算法线纹理贴图的 中 Shader 代码,粘贴到新建文件中

    具体代码,详见:US3S2L7——世界空间下计算法线纹理贴图

  3. 加入渲染标签 Tags { "RenderType"="Opaque" "Queue"="Geometry"}

    渲染类型设置为不透明的、渲染队列设置为几何队列(不透明的几何体通常使用该队列)

    1
    2
    3
    4
    5
    SubShader
    {
    Tags { "RenderType" = "Opaque" "Queue" = "Geometry" }
    Pass {/*...*/}
    }
  4. 加入阴影、衰减相关代码

  5. 加入附加渲染通道

    可以直接复用 Bass Pass 的内容,只是需要修改 LightMode​ 和编译指令,再加上线性混合,同时注意修改光源方向相关计算代码并删除环境光的叠加

  6. 加入 FallBack "Specular"​,实现阴影投射

完整 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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
Shader "TeachShader/BumpedSpecular"
{
Properties
{
_MainColor("MainColor", Color) = (1, 1, 1, 1)
_MainTex("MainTex", 2D) = ""{}
_BumpMap("BumpMap", 2D) = ""{}
_BumpScale("BumpScale", Range(0, 1)) = 1
_SpecularColor("SpecularColor", Color) = (1, 1, 1, 1)
_SpecularNum("SpecularNum", Range(0, 20)) = 18
}
SubShader
{
Tags { "RenderType" = "Opaque" "Queue" = "Geometry" }

// 基础渲染通道 Base Pass
Pass
{
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase

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

struct v2f
{
float4 pos: SV_POSITION;
float4 uv: TEXCOORD0; // 使用float4同时存储主要纹理的uv(xy存储)和法线纹理的uv(zw存储)
float4 tangentToWorld0: TEXCOORD1; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第一行
float4 tangentToWorld1: TEXCOORD2; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第二行
float4 tangentToWorld2: TEXCOORD3; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第三行
SHADOW_COORDS(4) // 阴影坐标变量宏,因为前面有4个TEXCOORD变量,故这里参数填写4
};

float4 _MainColor; //漫反射颜色
sampler2D _MainTex; //颜色纹理
float4 _MainTex_ST; //颜色纹理的缩放和平移
sampler2D _BumpMap; //法线纹理
float4 _BumpMap_ST; //法线纹理的缩放和平移
float _BumpScale; //凹凸程度
float4 _SpecularColor; //高光颜色
fixed _SpecularNum; //光泽度

v2f vert (appdata_full v)
{
v2f data;

data.pos = UnityObjectToClipPos(v.vertex); //计算裁剪空间下顶点坐标
// 分别计算主纹理和法线纹理的缩放平移
data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
data.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
// 得到世界空间下的顶点位置,用于之后在片元中计算视角方向(基于世界空间下)
float3 worldPos = mul(unity_ObjectToWorld, v.vertex);
// 将模型空间下的法线、切线转换到世界空间下,并计算世界空间下的副切线
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent);
float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * v.tangent.w;
// 将切线空间到世界空间的转换矩阵,以及世界坐标存储到三个贴图变量内
data.tangentToWorld0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
data.tangentToWorld1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
data.tangentToWorld2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
// 阴影坐标转换宏
TRANSFER_SHADOW(data)

return data;
}

fixed4 frag (v2f i) : SV_Target
{
// 计算世界空间下光的方向和视角方向
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
float3 worldPos = float3(i.tangentToWorld0.w, i.tangentToWorld1.w, i.tangentToWorld2.w);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// 通过法线纹理采样并解压缩,再乘以凹凸系数,得到切线空间下法线数据
float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// 将切线空间下法线数据转换到世界空间下
float3 worldNormal = float3(
dot(i.tangentToWorld0.xyz, tangentNormal),
dot(i.tangentToWorld1.xyz, tangentNormal),
dot(i.tangentToWorld2.xyz, tangentNormal)
);

// 根据以上数据计算光照模型颜色
fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _MainColor.rgb; //反射率
// 漫反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, lightDir));
// 高光反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
float3 halfA = normalize(viewDir + lightDir); //半角向量
fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfA)), _SpecularNum);
// 计算光照和阴影综合衰减值
UNITY_LIGHT_ATTENUATION(atten, i, worldPos);
// 最终颜色
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + (lambertColor + specularColor) * atten;

return fixed4(color.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 pos: SV_POSITION;
float4 uv: TEXCOORD0; // 使用float4同时存储主要纹理的uv(xy存储)和法线纹理的uv(zw存储)
float4 tangentToWorld0: TEXCOORD1; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第一行
float4 tangentToWorld1: TEXCOORD2; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第二行
float4 tangentToWorld2: TEXCOORD3; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第三行
SHADOW_COORDS(4) // 阴影坐标变量宏,因为前面有4个TEXCOORD变量,故这里参数填写4
};

float4 _MainColor; //漫反射颜色
sampler2D _MainTex; //颜色纹理
float4 _MainTex_ST; //颜色纹理的缩放和平移
sampler2D _BumpMap; //法线纹理
float4 _BumpMap_ST; //法线纹理的缩放和平移
float _BumpScale; //凹凸程度
float4 _SpecularColor; //高光颜色
fixed _SpecularNum; //光泽度

v2f vert (appdata_full v)
{
v2f data;

data.pos = UnityObjectToClipPos(v.vertex); //计算裁剪空间下顶点坐标
// 分别计算主纹理和法线纹理的缩放平移
data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
data.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
// 得到世界空间下的顶点位置,用于之后在片元中计算视角方向(基于世界空间下)
float3 worldPos = mul(unity_ObjectToWorld, v.vertex);
// 将模型空间下的法线、切线转换到世界空间下,并计算世界空间下的副切线
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent);
float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * v.tangent.w;
// 将切线空间到世界空间的转换矩阵,以及世界坐标存储到三个贴图变量内
data.tangentToWorld0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
data.tangentToWorld1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
data.tangentToWorld2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
// 阴影坐标转换宏
TRANSFER_SHADOW(data)

return data;
}

fixed4 frag (v2f i) : SV_Target
{
// 计算世界空间下光的方向和视角方向
float3 worldPos = float3(i.tangentToWorld0.w, i.tangentToWorld1.w, i.tangentToWorld2.w);
#if defined(_DIRECTIONAL_LIGHT)
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz); // 平行光的光源方向
#else
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz - worldPos); // 非平行光的光源方向
#endif
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// 通过法线纹理采样并解压缩,再乘以凹凸系数,得到切线空间下法线数据
float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// 将切线空间下法线数据转换到世界空间下
float3 worldNormal = float3(
dot(i.tangentToWorld0.xyz, tangentNormal),
dot(i.tangentToWorld1.xyz, tangentNormal),
dot(i.tangentToWorld2.xyz, tangentNormal)
);

// 根据以上数据计算光照模型颜色
fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _MainColor.rgb; //反射率
// 漫反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, lightDir));
// 高光反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
float3 halfA = normalize(viewDir + lightDir); //半角向量
fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfA)), _SpecularNum);
// 计算光照和阴影综合衰减值
UNITY_LIGHT_ATTENUATION(atten, i, worldPos);
// 最终颜色
fixed3 color = (lambertColor + specularColor) * atten;

return fixed4(color.rgb, 1);
}
ENDCG
}
}

Fallback "Specular"
}

显示效果:

image

可以看到,这个 Shader 拥有法线带来的凹凸效果,拥有多光源的光照效果,同时可以接收并投射阴影,相比单纯的漫反射还拥有高光效果