在构建这个shader之前,我们需要知道一个定律,兰伯特定律(Lambert’s low):反射光线的强度与表面法线和光源方向之间的夹角的cos成正比,因此漫反射的计算公式如下
n是表面法线,l是光源的单位矢量,max(0,n·l)是取0,n·l中的最大值,clight 是光源颜色,mdiffuse 是材质的漫反射颜色。
所以当我们要计算漫反射的颜色的时候,我们需要4个参数,物体的表面法线、光源的单位矢量、光源颜色与材质的漫反射颜色。由于本章所编写的是默认光线下的光照模型,所以只进行单一光照的计算,而表面法线与光源的单位矢量可以通过计算取得,所以我们只需定义表面漫反射的颜色即可。所以在属性声明中只需要进行漫反射颜色的定义。以下是我的shader代码。
Shader "Custom/DiffuseShader"
{
Properties
{
_Diffuse("Diffuse Color",Color)=(1,1,1,1)
}
SubShader
{
pass{
Tags{"LightModel"="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;
float3 color:COLOR;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));
fixed3 lightNormal=normalize(_WorldSpaceLightPos0);
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,lightNormal));
o.color=ambient.rgb+diffuse.rgb;
return o;
}
fixed4 frag(v2f i):SV_TARGET{
return fixed4(i.color,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
我们继续来逐行翻译,因为我们只是编写了一个漫反射模型,所以暂时不对贴图进行采样。我们在属性中只添加了一个颜色属性所对应的是漫反射颜色。
我们在进行漫反射计算的时候要正确的设置LightMode才能得到我们接下来需要用到的_LightColor0这个内置变量,LightMode指的是灯光的渲染路径,而ForwardBase则是设置灯光的渲染路径的一个标签,它决定了渲染器如何渲染灯光,其他的还有ForwardAdd等标签。接着就是正常的框架搭建,不在赘述,但我们这里运用到了一个头文件”Lighting.cginc”,该头文件主要是有一些内置的函数可以直接供我们调用,比如之前所说的_LightColor0这个变量。
fixed4 _Diffuse;
_Diffuse是颜色属性,我们按照惯例直接定义成fixed4类型。
我们从顶点着色器开始,首先编写结构体a2v,我们要想一想要构建漫反射模型需要从我们的顶点数据中取得什么数据?顶点位置自然不必多说,我们来回顾一下Lambert定律Cdiffuse =Lcolor *Mdiffuse *max(0,n*l)。环境光我们可以通过内置函数得到,漫反射材质的颜色由我们的意愿控制,剩下的就是表面法线与光的单位向量了。而光的单位向量我们可以通过Unity内置的函数取得,而表面法线则需要与顶点进行交互才能够取得所以,我们这里又用到一个语义:NORMAL,获取顶点的法线信息,所以我们的结构体如下
Struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
我们将NORMAL语义所取得的顶点法线信息存储在normal这个变量中,又因为法线是一个只包含XYZ的向量信息,所以我以float3来进行定义。
下面我们来构建顶点着色器,
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
...
}
v2f 定义了我们的输出参数是o,之后的就是一般设置。顶点的法线信息是基于模型坐标的,所以我们首先将其变换到空间坐标中,这里需要将顶点的法线与变换矩阵unity_WorldToObject(老版本为:_World2Object)进行mul计算,之后将其归一化,所以我们这么写
fixed3 worldNormal=normalize(mul(v.normal,unity_WorldToObject));
注意,这么些对么?首先法线是一个三维矢量,而我们的unity_WorldToObject则是一个4X4的矩阵,所以我们在进行变换的时候只需要截取前三行与前三列就行了,修改过后的代码
fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));
这样我们就得到了我们需要的n,接下来我们来进行l的计算,关于光源的方向我们可以直接使用内置函数_WorldSpaceLightPos0来取得,但请注意,这个函数只计算单一函数,所以在更复杂的光线中并不能计算出正真的光线信息,但在这个模型中是够用了。
fixed3 worldLight=normalize(_WorldSpaceLightPos0);
如此我们就得到了我们需要两个向量的归一化,worldNormal与worldLight,接着就很简单了,套入我们的Lambert定律就行了,再回一下Lambert定律:Cdiffuse=Lcolor*Mdiffuse*max(n,l)
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(worldNormal,worldLight);
至此,我们的漫反射计算结束,但整体的顶点着色器还没有完成,因为在考虑漫反射的时候不得不在意环境光。所以我们还需要调用一个内置函数UNITY_LIGHTMODEL_AMBIENT。
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
我们得到了漫反射颜色与环境光,将其相加则是最终我们所得到的漫反射模型颜色,最后返回o就可以了。所以完整的顶点着色器如下
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));
fixed3 lightNormal=normalize(_WorldSpaceLightPos0);
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,lightNormal));
o.color=ambient.rgb+diffuse.rgb;
return o;
}
恭喜,我们已经将最困难的部分完成了,接下来我们来编写片元着色器的结构体,首先我们跟之前一样,需要将裁剪过后的顶点坐标传输给pos,我们还仍需要顶一个color变量,color是作为o的输出与片元中的输入,我们可以赋予他COLOR,有些资料也可以赋予TEXCOORD0,这并不是写死的,可自由发挥,所以我们的结构体为
struct v2f{
pos:SV_POSITION;
color:COLOR;
};
接着是我们的片元着色器,这部分很简单,因为我们将所有的工作都在顶点着色其中做完了,所以直接输出顶点信息就可以了
fixed4 frag(v2f i):SV_Target{
return fixed4 (i.color,1.0)
}
注意,由于我们在顶点着色器中输出的颜色是rgb是一个三位信息,所以我们添加个1.0将其补完。
至此,我们完成了我们整个的Lambert光照模型,下一节我们将结合贴图与这个模型进行一个带有漫反射性质的贴图模型。
接下来我们在shader graph实现这个漫反射模型
大体思路跟我们写代码的思路一样,但值得提的一点是shader graph中并没有默认光源位置的获取,所以我们要添加一个自定义节点来获取光的颜色以及矢量
#if SHADERGRAPH_PREVIEW
Direction=half3(0.5,0.5,0); Color=1;
#else
Light light=GetMainlight();
Direction=light.direction;
Color=light.color;
#endif
在实际的制作过程中,这个代码非常常用,所以我直接贴出来吧。
接下来我们在UE4中复现我们的操作。SD主要是进行材质的编写,并不涉及管线的编写,所以这篇我们就略过了。