Unity Shader 实现光照模型
实现漫反射光照模型
漫反射部分计算公式:cdiffuse = (clight · mdiffuse)max(0,n · I);
计算漫反射需要的4个参数:
1. 入射光线的颜色和强度clight ;
2. 材质的漫反射系数mdiffuse;
3. 表面法线n;
4. 光源方向I;
防止点积结果负值CG中提供函数saturate函数
函数:saturate(x)
参数:x:为用于操作的标量或矢量,可以是float、float2、float3等类型;
描述:把x截取在[0,1]范围内,如果x是一个矢量,那么会对它的每一个分量进行这样的操作。
逐顶点光照
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Shader Test/Chapter 6/Diffuse Vertex-Level"
{
// 为了得到且控制材质的漫反射贪色,声明一个Color类型的属性并把初始值设置为白色
Properties
{
_Diffuse("Diffuse",Color) = (1,1,1,1)
}
SubShader
{
// 顶点片元着色器要写在 Pass 语义块中
Pass
{
// 指明该 Pass 的光照模式
// LightMode标签是Pass标签的一种,用于定义该Pass在Unity光照流水线中的角色
// 只有定义了正确的LightMode才能得到一些Unity的内置光照变量
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 使用Unity的一些内置变量需包含进Unity的Lighting.cginc文件
#include "Lighting.cginc"
// 为使用 Properties 语义块中声明的属性,我们需要定义一个和该属性类型相匹配的变量
// 通过这种方式可获得漫反射公式的参数材质的漫反射属性。由于颜色属性范围在 0-1 ,因此使用fixed的变量存储
fixed4 _Diffuse;
// 定义顶点着色器输入输出结构体(输出结构体也是片元着色器的输入结构体)
struct a2v {
float4 vertex : POSITION;
// 为访问顶点法线,通过NORMAL语义告诉Unity要把模型顶点的法线信息存储到normal变量中
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
// 为把在顶点着色器中计算的光照颜色传递给片元着色器,需定义一个color变量,且并不是必须使用COLOR,一些资料使用TEXCOORD0语义
fixed3 color : COLOR;
};
// 逐顶点的漫反射光照计算都将在顶点着色器中进行
v2f vert(a2v v)
{
v2f o;
// 坐标转换把顶点位置从模型空间转换到裁剪空间中
o.pos = UnityObjectToClipPos(v.vertex);
// Unity内置变量 UNITY_LIGHTMODEL_AMBIENT 得到了环境光部分
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 计算法线和光源方向的点积时两者需在同意坐标系,在这使用世界坐标空间
// a2v中的顶点法线时位于模型空间下的,需要转换到世界空间中
// 使用顶点变换矩阵的逆转置矩阵对法线进行变换,得到模型空间到世界空间的变换矩阵的逆矩阵_World2Object
// 调换mul参数位置,得到和转置矩阵相同的矩阵乘法。法线是三维矢量,_World2Object只需截取前三和前三列,
// 得到世界空间中的法线和光源方向需要进行归一化处理
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
// _WorldSpaceLightPos0:提供光源方向
// (注:对光源方向的计算并不具有通用性,在这使用的是平行光且光源唯一,
// 若多个元且光源类型可能是其他类型直接使用_WorldSpaceLightPos0 就不能得到正确的结果)
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// _LightColor0:Unity提供的内置变量,用来访问该Pass处理的光源的颜色和强度信息(注:想要得到正确的值需要定义合适的LightMode标签)
// 得到世界空间中的法线和光源方向的点积结果后需防止结果为负值,saturate函数吧参数截取到[0,1]的范围内
// 最后将光源的颜色和强度以及材质的漫反射颜色相乘得到最终漫反射光照部分
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
// 最后把环境光和漫反射光部分相加得到最终光照结果
o.color = ambient + diffuse;
return o;
}
// 计算在顶点着色器完成,片元着色器只需直接把顶点颜色输出即可
fixed4 frag(v2f i) :SV_Target
{
return fixed4(i.color, 1.0);
}
ENDCG
}
}
// 把Unity Shader 的回调 shader 设置为内置的 Diffuse
Fallback "Diffuse"
}
对细分程度较高的模型,逐顶点光照已经可以得到比较好的光照效果了。但对于一些细分程度较低的模型,逐顶点光照就会出现一些视觉问题,例如在背光面与向光面交界处有一些锯齿。为了解决这些问题可以使用逐像素的漫反射光照。
逐像素光照
Shader "Shader Test/Chapter 6/Diffuse Pixel-Level"
{
// 为了得到且控制材质的漫反射贪色,声明一个Color类型的属性并把初始值设置为白色
Properties
{
_Diffuse("Diffuse",Color) = (1,1,1,1)
}
SubShader
{
// 顶点片元着色器要写在 Pass 语义块中
Pass
{
// 指明该 Pass 的光照模式
// LightMode标签是Pass标签的一种,用于定义该Pass在Unity光照流水线中的角色
// 只有定义了正确的LightMode才能得到一些Unity的内置光照变量
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 使用Unity的一些内置变量需包含进Unity的Lighting.cginc文件
#include "Lighting.cginc"
// 为使用 Properties 语义块中声明的属性,我们需要定义一个和该属性类型相匹配的变量
// 通过这种方式可获得漫反射公式的参数材质的漫反射属性。由于颜色属性范围在 0-1 ,因此使用fixed的变量存储
fixed4 _Diffuse;
// 定义顶点着色器输入输出结构体(输出结构体也是片元着色器的输入结构体)
struct a2v {
float4 vertex : POSITION;
// 为访问顶点法线,通过NORMAL语义告诉Unity要把模型顶点的法线信息存储到normal变量中
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
};
v2f vert(a2v v)
{
v2f o;
// 坐标转换把顶点位置从模型空间转换到裁剪空间中
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
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 * saturate(dot(worldNormal, worldLightDir));
fixed3 color = ambient + diffuse;
return fixed4(color, 1.0);
}
ENDCG
}
}
// 把Unity Shader 的回调 shader 设置为内置的 Diffuse
Fallback "Diffuse"
}
逐像素光照可以得到更加平滑的光照效果。
但即便使用了逐像素漫反射光照,仍有一个问题,在光照无法到达的区域,模型的外观通常是全黑的,没有任何明暗变化,这会使模型的背光区域看起来就像一个平面一样,失去了模型细节表现。
可以通过添加环境光来得到非全黑的效果,但即便这样仍然无法解决背光面明暗一样的缺点。为此,有一种改善技术被提出来,这就是**半兰伯特(Half Lambert)**光照模型。
半兰伯特模型
兰伯特定律:在平面某点漫反射光的光强与该反射点的法向量和入射光角度的余弦值成正比。
半兰伯特光照模型是在原兰伯特光照模型的基础上进行了简单的修改;
广义的半兰伯特光照模型的公式:cdiffuse = (clight · mdiffuse)(
α
\alpha
α(0, n · I) +
β
\beta
β)
半兰伯特光照模型没有使用max操作防止**
n
^
\widehat{n}
n
** 和 I的点积为负值,而是对其结果进行
α
\alpha
α倍的缩放在加上一个
β
\beta
β大小的偏移;绝大多数情况下,
α
\alpha
α和
β
\beta
β的值均为0.5,即公式:cdiffuse = (clight · mdiffuse)(0.5(0, n · I) + 0.5)
Shader "Shader Test/Chapter 6/Half Lambert"
{
// 为了得到且控制材质的漫反射贪色,声明一个Color类型的属性并把初始值设置为白色
Properties
{
_Diffuse("Diffuse",Color) = (1,1,1,1)
}
SubShader
{
// 顶点片元着色器要写在 Pass 语义块中
Pass
{
// 指明该 Pass 的光照模式
// LightMode标签是Pass标签的一种,用于定义该Pass在Unity光照流水线中的角色
// 只有定义了正确的LightMode才能得到一些Unity的内置光照变量
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 使用Unity的一些内置变量需包含进Unity的Lighting.cginc文件
#include "Lighting.cginc"
// 为使用 Properties 语义块中声明的属性,我们需要定义一个和该属性类型相匹配的变量
// 通过这种方式可获得漫反射公式的参数材质的漫反射属性。由于颜色属性范围在 0-1 ,因此使用fixed的变量存储
fixed4 _Diffuse;
// 定义顶点着色器输入输出结构体(输出结构体也是片元着色器的输入结构体)
struct a2v {
float4 vertex : POSITION;
// 为访问顶点法线,通过NORMAL语义告诉Unity要把模型顶点的法线信息存储到normal变量中
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
};
v2f vert(a2v v)
{
v2f o;
// 坐标转换把顶点位置从模型空间转换到裁剪空间中
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
return o;
}
fixed4 frag(v2f i) :SV_Target
{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;
fixed3 color = ambient + diffuse;
return fixed4(color, 1.0);
}
ENDCG
}
}
// 把Unity Shader 的回调 shader 设置为内置的 Diffuse
Fallback "Diffuse"
}
实现高光反射光照模型
高光反射部分的计算公式:cspecular = (clight · mspecular)max(0, v ^ \widehat{v} v · r)mgloss
计算高光反射需要的4个参数:
1. 入射光线的颜色和强度clight;
2. 材质的高光反射系数mspecular;
3. 视觉方向**
v
^
\widehat{v}
v
;
4. 反射方向r**,这可由表面法线**
n
^
\widehat{n}
n
和光源方向
I
^
\widehat{I}
I
计算而得:r = 2(
n
^
\widehat{n}
n
** ·
I
^
\widehat{I}
I
)
n
^
\widehat{n}
n
-
I
^
\widehat{I}
I
;
CG提供了计算反射方向的函数 reflect
函数:reflect(i, n)
参数:i,入射方向;n,法线方向。可以是float、float2、float3等类型。
描述:当给定入射方向i和法线方向n时,reflect函数可以返回反射方向。
逐顶点光照
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Shader Test/Chapter 6/Specular Vertex-Level"
{
Properties
{
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
// _Specular 用于控制材质的高光反射颜色
_Specular("Specular", Color) = (1, 1, 1, 1)
// _Gloss用于控制高光区域的大小
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Pass
{
// 指明该 Pass 的光照模式
// LightMode标签是Pass标签的一种,用于定义该Pass在Unity光照流水线中的角色
// 只有定义了正确的LightMode才能得到一些Unity的内置光照变量
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 使用Unity的一些内置变量需包含进Unity的Lighting.cginc文件
#include "Lighting.cginc"
// 为使用 Properties 语义块中声明的属性,我们需要定义一个和该属性类型相匹配的变量
// 颜色属性的范围在 0-1,因此对 _Diffuse 和 _Specular 属性可使用 fixed 精度的变量来存储,_Gloss 范围很大可使用float精度来存储
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
// 定义顶点着色器输入输出结构体(输出结构体也是片元着色器的输入结构体)
struct a2v {
float4 vertex : POSITION;
// 为访问顶点法线,通过NORMAL语义告诉Unity要把模型顶点的法线信息存储到normal变量中
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
// 为把在顶点着色器中计算的光照颜色传递给片元着色器,需定义一个color变量,且并不是必须使用COLOR,一些资料使用TEXCOORD0语义
fixed3 color : COLOR;
};
v2f vert(a2v v)
{
v2f o;
// 坐标转换把顶点位置从模型空间转换到裁剪空间中
o.pos = UnityObjectToClipPos(v.vertex);
// Unity内置变量 UNITY_LIGHTMODEL_AMBIENT 得到了环境光部分
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
/// 漫反射部分-----
// 计算法线和光源方向的点积时两者需在同意坐标系,在这使用世界坐标空间
// a2v中的顶点法线时位于模型空间下的,需要转换到世界空间中
// 使用顶点变换矩阵的逆转置矩阵对法线进行变换,得到模型空间到世界空间的变换矩阵的逆矩阵_World2Object
// 调换mul参数位置,得到和转置矩阵相同的矩阵乘法。法线是三维矢量,_World2Object只需截取前三和前三列,
// 得到世界空间中的法线和光源方向需要进行归一化处理
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
// _WorldSpaceLightPos0:提供光源方向
// (注:对光源方向的计算并不具有通用性,在这使用的是平行光且光源唯一,
// 若多个元且光源类型可能是其他类型直接使用_WorldSpaceLightPos0 就不能得到正确的结果)
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// _LightColor0:Unity提供的内置变量,用来访问该Pass处理的光源的颜色和强度信息(注:想要得到正确的值需要定义合适的LightMode标签)
// 得到世界空间中的法线和光源方向的点积结果后需防止结果为负值,saturate函数吧参数截取到[0,1]的范围内
// 最后将光源的颜色和强度以及材质的漫反射颜色相乘得到最终漫反射光照部分
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
/// --------------------
/// 高光反射部分-----
// 计算入射光线方向关于表面法线的反射方向 reflectDir
// reflect 函数的入射方向要求是由光源指向交点处,所以需对 worldLightDir 取反传入
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
// 通过 _WorldSpaceCameraPos 得到了世界空间中的摄像机位置,再把顶点位置从模型空间转换到世界空间下,相减即得到世界空间下的视角方向
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);
// 计算高光反射光照部分
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
/// --------------------
// 最后把环境光,漫反射光照部分和高光反射光照部分相加得到最终光照结果
o.color = ambient + diffuse + specular;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
return fixed4(i.color, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
逐顶点的方法得到的高光效果有较大的问题:不平滑;
原因:高光反射部分的计算是非线性的,而在顶点着色器中计算光照再进行插值的过程是线性的,破坏了原计算的非线性关系,就会出现较大的视觉问题。
逐像素光照
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Shader Test/Chapter 6/Specular Pixel-Level"
{
// 为了得到且控制材质的漫反射贪色,声明一个Color类型的属性并把初始值设置为白色
Properties
{
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
// _Specular 用于控制材质的高光反射颜色
_Specular("Specular", Color) = (1, 1, 1, 1)
// _Gloss用于控制高光区域的大小
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
// 顶点片元着色器要写在 Pass 语义块中
Pass
{
// 指明该 Pass 的光照模式
// LightMode标签是Pass标签的一种,用于定义该Pass在Unity光照流水线中的角色
// 只有定义了正确的LightMode才能得到一些Unity的内置光照变量
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 使用Unity的一些内置变量需包含进Unity的Lighting.cginc文件
#include "Lighting.cginc"
// 为使用 Properties 语义块中声明的属性,我们需要定义一个和该属性类型相匹配的变量
// 颜色属性的范围在 0-1,因此对 _Diffuse 和 _Specular 属性可使用 fixed 精度的变量来存储,_Gloss 范围很大可使用float精度来存储
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
// 定义顶点着色器输入输出结构体(输出结构体也是片元着色器的输入结构体)
struct a2v {
float4 vertex : POSITION;
// 为访问顶点法线,通过NORMAL语义告诉Unity要把模型顶点的法线信息存储到normal变量中
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 * saturate(dot(worldNormal, worldLightDir));
/// --------------------
/// 高光反射部分-----
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
/// --------------------
fixed3 color = ambient + diffuse + specular;
return fixed4(color, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
逐像素的方式处理光照可得到更加平滑的高光效果,至此已实现一个完整的 Phong 光照模型。
Blinn-Phong 光照模型
以上为 Phong 光照模型在 Unity 中的实现,有另一种高光反射的实现方法 —— Blinn 光照模型
Blinn 模型没有使用反射方向,而是引入一个新的矢量
h
^
\widehat{h}
h
,它是通过对视觉方向**
v
^
\widehat{v}
v
和光照方向
I
^
\widehat{I}
I
**相加后在归一化得到的,即:
h
^
=
v
^
+
I
∣
v
^
+
I
∣
\widehat{h}=\frac{\widehat{v} + I}{|\widehat{v} + I|}
h
=∣v
+I∣v
+I
Blinn模型计算高光反射的公式:cspecular = (clight · mspecular)max(0,
n
^
\widehat{n}
n
·
h
^
\widehat{h}
h
)mgloss
Shader "Shader Test/Chapter 6/Blinn Phong"
{
// 为了得到且控制材质的漫反射贪色,声明一个Color类型的属性并把初始值设置为白色
Properties
{
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
// _Specular 用于控制材质的高光反射颜色
_Specular("Specular", Color) = (1, 1, 1, 1)
// _Gloss用于控制高光区域的大小
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
// 顶点片元着色器要写在 Pass 语义块中
Pass
{
// 指明该 Pass 的光照模式
// LightMode标签是Pass标签的一种,用于定义该Pass在Unity光照流水线中的角色
// 只有定义了正确的LightMode才能得到一些Unity的内置光照变量
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 使用Unity的一些内置变量需包含进Unity的Lighting.cginc文件
#include "Lighting.cginc"
// 为使用 Properties 语义块中声明的属性,我们需要定义一个和该属性类型相匹配的变量
// 颜色属性的范围在 0-1,因此对 _Diffuse 和 _Specular 属性可使用 fixed 精度的变量来存储,_Gloss 范围很大可使用float精度来存储
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
// 定义顶点着色器输入输出结构体(输出结构体也是片元着色器的输入结构体)
struct a2v {
float4 vertex : POSITION;
// 为访问顶点法线,通过NORMAL语义告诉Unity要把模型顶点的法线信息存储到normal变量中
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 * saturate(dot(worldNormal, worldLightDir));
/// --------------------
/// 高光反射部分-----
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
/// --------------------
fixed3 color = ambient + diffuse + specular;
return fixed4(color, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
Blinn-Phong 光照模型的高光反射部分看起来更大、更亮一些。实际渲染中绝大多数情况会选择Blinn-Phong 光照模型。
!注:两者都是经验模型,实际上在一些情况下,Blinn-Phong 光照模型更符合实验结果。
Unity 内置的函数
在计算光照模型的时候,往往需要得到光源方向、视角方向这两个基本信息。
在上面的实现中都是自行在代码里计算的,例如使用 normalize(WorldSpaceLightPos0.xyz) 来得到光源方向(这种方法实际只适用于平行光),使用 normalize(WorldSpaceCameraPos.xyz - i.worldPosition.xyz) 来得到视角方向。
但如果需要处理更复杂的光照类型,如点光源和聚光灯,这种计算光源方向的方法就是错误的。这需要在代码中先判断光源类型,再计算它的光源信息。
手动计算这些光源信息的过程相对比较麻烦,Unity提供了一些内置函数来帮助我们计算这些信息。
类似UnityXXX的几个函数是 Unity5 中新添加的内置函数。这些帮助函数使得我们不需要跟各种变换矩阵、内置变量打交道,也不需要考虑各种不同的情况(例如使用了哪种光源),而仅仅调用一个函数就可以得到需要的信息。
WorldSpaceViewDir 函数实现如图:
与之前计算视觉方向的方法一致,需注意的是,这些函数都没有保障得到的方向矢量是单位矢量,需要在使用前进行归一化处理。
计算光源的 3 个函数:WorldSpaceLightDir、UnityWorldSpaceLightDir 和 ObjectSpaceLightDir 复杂一些,因为Unity处理了不同种类光源的情况。
需注意的是,这3个函数仅可用于前向渲染。
原因:只有在前向渲染时,这3个函数里使用的内置变量 _WorldSpaceLightPos0 等才会被正确赋值。
使用内置函数改写 Blinn-Phong 光照模型的 Unity Shader:
Shader "Shader Test/Chapter 6/Blinn Phong Use Build In Function"
{
// 为了得到且控制材质的漫反射贪色,声明一个Color类型的属性并把初始值设置为白色
Properties
{
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
// _Specular 用于控制材质的高光反射颜色
_Specular("Specular", Color) = (1, 1, 1, 1)
// _Gloss用于控制高光区域的大小
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
// 顶点片元着色器要写在 Pass 语义块中
Pass
{
// 指明该 Pass 的光照模式
// LightMode标签是Pass标签的一种,用于定义该Pass在Unity光照流水线中的角色
// 只有定义了正确的LightMode才能得到一些Unity的内置光照变量
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 使用Unity的一些内置变量需包含进Unity的Lighting.cginc文件
#include "Lighting.cginc"
// 为使用 Properties 语义块中声明的属性,我们需要定义一个和该属性类型相匹配的变量
// 颜色属性的范围在 0-1,因此对 _Diffuse 和 _Specular 属性可使用 fixed 精度的变量来存储,_Gloss 范围很大可使用float精度来存储
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
// 定义顶点着色器输入输出结构体(输出结构体也是片元着色器的输入结构体)
struct a2v {
float4 vertex : POSITION;
// 为访问顶点法线,通过NORMAL语义告诉Unity要把模型顶点的法线信息存储到normal变量中
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 = UnityObjectToWorldNormal(v.normal);//***********
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(UnityWorldSpaceLightDir(i.worldPos));//***********
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(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);
/// --------------------
fixed3 color = ambient + diffuse + specular;
return fixed4(color, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
//*********** 脚本代码末尾有此标注的为替换为内置方法的代码。
需注意的是,由内置函数得到的方向是没有归一化的,因此需要使用 normalize 函数来对结果进行归一化,再进行光照模型的计算。