在计算光照模型时,通常来讲会有两种:在顶点着色器中计算的称为逐顶点光照;在片元着色器中计算的被称为逐像素光照,我们先讲逐顶点光照。
逐顶点光照也被称为高洛德着色,我们在每个顶点上计算光照,然后会在渲染图元内部进行线性插值,最后输出成像素颜色。由于顶点数目往往远小于像素数目,因此逐顶点光照的计算量往往要小于逐像素光照。但是由于逐顶点光照依赖于线性插值来得到像素光照,因此当光照模型中有非线性的计算(如计算高光反射时),逐顶点光照就会出现问题。且逐顶点光照会在渲染图元内部对顶点颜色进行插值,会导致渲染图源内部的颜色总是暗于顶点处的最高颜色值,这在某些情况下会产生明显的棱角现象。
在Unity Shader中实现漫反射光照模型,计算公式:
为了防止点积结果为负值,我们需要使用max操作,在CG中提供了saturate(x)函数能达到同样的效果。
Shader "Unlit/Diffuse Vertex-Level" {
Properties {
//声明一个Color类型的属性,初始值设为白色
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
//设置光照模式,只有定义正确的光照模式才能得到Unity的内置光照变量
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//需要使用Unity内置一些变量,如_LightColor0
#include "Lighting.cginc"
//定义一个和Properties语义块中类型匹配的变量,得到材质的漫反射属性
fixed4 _Diffuse;
//顶点着色器的输入结构体
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);
//通过Unity内置变量得到环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//计算法线和光源方向的点积时,要在同一坐标空间下
//将法线从模型空间转换到世界空间
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
//得到光源方向
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
//计算漫反射(_LightColor0是Unity内置变量)
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"
}
然后是逐像素光照,我们会以每个像素为基础,得到它的法线,然后进行光照模型的计算。
Shader "Unlit/Diffuse Pixel-Level" {
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 {
//通过Unity内置变量获得光照强度
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"
}
逐像素光照可以得到更加平滑的光照效果,但是即使使用了逐像素漫反射光照,还有一个问题仍然存在,在光照无法到达的区域,模型的外观通常是全黑的,没有任何明暗变化,会使得模型背光区域看起来就像一个平面一样,失去了模型细节表现。我们可以用一种改善技术来解决,就是半兰伯特光照模型。
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);
}
直接用半兰伯特公式修改片元着色器中计算漫反射光照的部分即可。