最近在学习《Unity Shader入门精要》一书,学到了光照模型,正好自己以前也用过OpenGL实现过标准光照模型(也可以叫Phong光照模型),那么这次就用Unity Shader实现一下标准光照模型吧。
那么首先做一下准备工作:
至此,准备工作已经完成,现在开始计算漫反射。漫反射使用兰伯特(Lambert)光照模型表示,公式为
其中Kd为材质的漫反射颜色,lightColor为光照颜色,N, L分别为表面法线,指向光源的单位矢量,dot为向量点乘,max的作用是保证法线方向和光源方向的点乘不为负值,意义就是防止物体被从后面来的光源照亮。
虽然漫反射公式已经知道,那么是在顶点着色器计算它的颜色还是在片元着色器计算它的颜色呢?答案自然是两者都可以。其中在片元中计算就是逐像素光照,在顶点中计算就是逐顶点光照,对于下面的高光反射的计算也是如此。
逐顶点计算漫反射:
Shader "Unity Shaders Book/DiffuseVertexLevel"
{
Properties{
_Diffuse("Diffuse",Color)=(1,1,1,1) //声明一个Color属性用来表示漫反射颜色,可以在Inspector面板显示
}
SubShader{
Pass{
Tags{"LightMode"="ForwardBase"} //定义该Pass在光照流水线中的角色,只有定义正确才能获得一些内置光照变量
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//Lighting.cginc为包含一些内置光照变量的头文件
fixed4 _Diffuse; //定义一个在Properties中声明过的变量,这样才可以使用它
struct a2v //顶点着色器输入结构体
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f //顶点着色器输出结构体,片元着色器输入结构体
{
float4 pos : SV_POSITION;
fixed3 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 worldLight=normalize(_WorldSpaceLightPos0.xyz);//获取平行光的方向(只适用只有一个平行光源的时候)
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLight));//计算漫反射,其中_LightColor0只有正确声明了Tags和包含正确的头文件才可以获取
o.color=ambient+diffuse;
return o;
}
fixed4 frag(v2f i):SV_Target//表面着色器
{
return fixed4(i.color,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
逐像素计算漫反射:
Shader "Unlit/DiffusePixelLevel"
{
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;
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 worldLgihtDir=normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLgihtDir));
fixed3 color=ambient+diffuse;
return fixed4(color,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
对比可见,两种计算方式并没有太大的区别,区别只是在于颜色的计算位置。逐顶点的颜色计算是在顶点着色器中,得到的是顶点光照颜色,然后片元着色器通过线性插值输出像素的颜色,逐片元的是以每个像素基础,得到它的法线(这里是对顶点法线插值得到),然后再计算颜色。
至此,漫反射光照模型已经建立完成,但是由于max函数的作用,物体的背光面会是完全的黑色,这显然与现实不符合,因此就有了半兰伯特模型。它使用一个α倍的缩放和一个β倍的偏移来保证其不会为负值,这两个数绝大部分时候为0.5.
计算公式:
漫反射光照 = (光照颜色与强度 * 漫反射颜色)* (dot(法线方向 ,光照方向) * α + β);
因此使用半兰伯特模型的逐像素漫反射为:
Shader "Unlit/DiffuseHalfLambert"
{
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;
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 worldLgihtDir=normalize(_WorldSpaceLightPos0.xyz);
fixed halfLambert=dot(worldNormal,worldLgihtDir)*0.5+0.5;
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*halfLambert;
fixed3 color=ambient+diffuse;
return fixed4(color,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
最终三种效果为:
从里到外分别为逐顶点光照,逐像素光照,半兰伯特逐像素光照,可以看出逐顶点的黑白交界处有着明显的锯齿,逐像素的则更加平滑,半兰伯特的黑白分别没有前面两个的那么明显。
现在,我们来计算一下高光反射模型,也可以说是Phong光照模型,计算公式为:
其中Ks 为高光反射颜色, lightColor为光照颜色,view为相机方向,reflect为入射光反射方向,gloss为材质的光泽度,用来控制高光区域的大小,光泽度越大,区域越小。
同样的,这个光照模型也有两种方式:逐顶点着色,逐像素着色。
Phong模型的逐顶点着色(其中反射方向用reflect函数求得)
Shader "Unlit/SpecularVertexLevel"
{
Properties
{
_Diffuse("Diffuse",Color)=(1,1,1,1)
_Specular("Specular",Color)=(1,1,1,1)
_Gloss("Gloss",Range(8.0,256))=20
}
SubShader
{
Pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
fixed3 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 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-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.0f);
}
ENDCG
}
}
FallBack "Specular"
}
Phong模型的逐像素着色(其中反射方向用reflect函数求得)
Shader "Unlit/SpecularPixelLevel"
{
Properties
{
_Diffuse("Diffuse",Color)=(1,1,1,1)
_Specular("Specular",Color)=(1,1,1,1)
_Gloss("Gloss",Range(8.0,256))=20
}
SubShader
{
Pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v{
float4 vertex:POSITION;
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);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
FallBack "Specular"
}
某些情况下计算更快,更正确的光照模型——Blinn光照模型。
这是一种对Phong光照模型进行了修改的模型,并不是Phong模型的改进,它引入了一个新的矢量h ,目的是避免计算反射方向reflect,计算方式为:
其中view为相机方向,light为光源入射方向 。
那么使用了Blinn光照模型的逐像素着色
Shader "Unlit/BlinnPhong"
{
Properties
{
_Diffuse("Diffuse",Color)=(1,1,1,1)
_Specular("Specular",Color)=(1,1,1,1)
_Gloss("Gloss",Range(8.0,256))=20
}
SubShader
{
Pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v{
float4 vertex:POSITION;
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)_World2Object);
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(_WorldSpaceLightPos0.xyz);
fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
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 viewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir=normalize(worldLightDir+viewDir);
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(worldNormal,halfDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
FallBack "Specular"
}
最终效果为: 从里到外分别为逐顶点着色,逐像素着色,Bling模型的逐像素着色,其中漫反射颜色为浅黑色,这是为了更好的看出高光反射效果。由图可以看出逐顶点着色并不是一个圆形,这是对颜色进行线性插值的结果,但是高光反射的计算是非线性的。而使用Bling-Phong光照模型的高光部分更大更亮。 至此一个完整的标准光照模型就此完成,尽管标准光照模型并不是很符合现实,但是其简单,易于计算,更容易上手。