6.4在Unity Shader中实现漫反射光照模型
基本光照模型中的漫反射部分的计算公式:
漫反射光照模型符合兰伯特定律:平面上一个点的漫反射光照强度与其法线与入射光夹角的余弦值呈正比。
由上式可以得出,计算漫反射需要四个参数,分别是clight:入射光线的强度以及方向、mdiffuse:材质的漫反射系数、n:顶点法线、l:光源方向。截断负值除了max函数外,还可以使用CG函数saturate
用法:saturate(x)
参数:x,可以是矢量或者标量,类型可以为float, float2, float3
作用:把x截取在[0,1]之间,如果x是矢量,就对x的每一个分量这样操作。
6.4.1逐顶点光照
//起名+路径
Shader "Unity Shader Book/Chapter 6/Diffuse Vertex-Level"
{
Properties
{
//控制材质的漫反射颜色,初始值设为白色
_Diffuse ("Diffuse", Color) = (1,1,1,1)
}
SubShader
{
//顶点/片元着色器的代码需要在Pass语块
Pass
{
//LightMode 标签是Pass标签的一种,它用于定义该Pass在Unity的光照流水线中的角色。
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
//#pragma指令来告诉Unity顶点着色器和片元着色器的名字
#pragma vertex vert
#pragma fragment frag
//为了使用Unity内置的一些变量,需要包含进Unity的内置文件
#include "Lighting.cginc"
//为了在Shader中使用Properties语义块中声明的属性,我们需要定义一个和该属性类型相匹配的变量
//通过这样的方式,我们就可以得到漫反射公式中需要的参数之一——材质的漫反射属性。
//由于颜色属性的范围在0到1之间,因此我们可以使用fixed精度的变量来存储它。
fixed4 _Diffuse;
//定义顶点着色器的输入结构体
//为了访问顶点的法线,我们需要再a2v中定义一个normal变量,并通过使用NORMAL语义来告诉Unity
//要把模型顶点的法线信息存储到normal变量中。
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
//定义顶点着色器的输出结构体(同时也是片元着色器的输入结构体)
//为了把在顶点着色器中计算得到的光照颜色传递给片元着色器,我们需要在v2f中定义一个color变量,且并不是必须使用COLOR语义
struct v2f
{
float4 pos : SV_POSITION;
fixed3 color : COLOR;
};
//这是一逐顶点的漫反射光照,因此漫反射部分的计算都将在顶点着色器中进行
v2f vert(a2v v)
{
//定义返回值o
v2f o;
//顶点着色器最基本任务就是把顶点位置从模型空间转换到裁剪空间中,因此需要用矩阵来进行变换
o.pos = UnityObjectToClipPos(v.vertex);
//我们通过Unity的内置变量UNITY_LIGHTMODEL_AMBIENT 得到了环境光部分
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//然后开始真正计算漫反射光照部分(需要四个参数)
//我们已经知道了漫反射颜色_Diffuse和顶点法线v.normal
//还需要知道光源颜色和强度信息(想得到合适的值需要定义合适的LightMode标签)
//而光源方向可以由_WorldSpaceLightPos0 来得到。需要注意的是,这里对光源方向的计算并不具有通用性
// 在计算法线和光源方向之间的点积时,我们需要选择它们所在的坐标系,只有两者处于同一坐标空间下,它们的点积才有意义。
//在这里,我们选择了世界坐标空间。而由a2v得到的顶点法线是处于模型空间下的,因此我们首先需要把法线转换到世界空间中。
//在第4章中,我们已经知道可以使用顶点变换矩阵的逆转置对法线进行相同的变换,因此我们首先得到模型空间到世界空间的
//变换矩阵的逆矩阵_World2Object,然后通过调换它在mul函数中的位置,得到和转置矩阵相同的矩阵乘法。
//由于法线是一个三维矢量,因此我们只需要截取_World2Object的前三行前三列即可。
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
//在得到了世界空间中的法线和光源方向后,我们需要对它们进行归一化操作。在得到它们点击的结果后,我们使用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
}
}
Fallback "Diffuse"
}
6.4.2逐像素光照
Shader "Unity Shader Book/Chapter 6/Diffuse Pixel Level Mat"
{
Properties
{
_Diffuse ("Diffuse", Color) = (1,1,1,1)
}
SubShader
{
Pass
{
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
//修改顶点着色器
struct v2f
{
float4 pos : SV_POSITION;
fixed3 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
}
}
Fallback "Diffuse"
}
左:逐顶点光照 右:逐像素光照
逐像素光照的效果可以比逐顶点光照达到更加平滑的效果,但是在暗部(光照无法达到的区域)是全黑的,没有任何的渐变,背光区域像是一个平面,缺失了细节。虽然可以通过增加环境光来进行改善,但是依旧改变不了背光面明暗一样。由此产生了在原兰伯特模型上出现的半兰伯特模型。
6.4.3 半兰伯特模型
广义的半兰伯特光照模型公式:
通常情况下,该公式为:
这种方法可以将法线与入射光线的点积结果从[-1,1]映射到[0,1]内,使背光区域也有了明暗变化。
但是半兰伯特没有任何物理依据,只是一个视觉加强技术。
Shader "Unity Shader Book/Chapter 6/Half Lambert"
{
Properties
{
_Diffuse ("Diffuse", Color) = (1,1,1,1)
}
SubShader
{
Pass
{
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
fixed3 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);
//Compute diffuse term
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
}
}