在之前的文章中写的Shader,呈现出来的物体样子是一个平面2D的状态,即使物体是3D的,那是因为,我们还没有将灯光加入到Shader的运算中。现在,我们将介绍灯光相关的东西,最后呈现出和 Unity Diffuse Shader 一样的效果。
什么是光照模型
光照模型,简单理解就是一种运算,或者说一个公式,计算的结果,决定了一个点受到光照时,所表现出来的效果。例如,光照在木板上,和照在一面镜子上,我们所看到的效果是不一样的,照在镜子上,很大一部分光会被镜子反射,而木板,却不会反射那么多光。
进入摄相机的光线分类
在游戏中,我们可以将进入摄相机的光分为 高光反射
、漫反射
、自发光
等。像上面说的镜子反射了大部分光,就是高光反射,现实中比较光滑的表面,受到光照时,都会产生这种效果,很亮。而光线照在木头上,就是漫反射,其实是木头先吸收了光,然后向周围散射出去,这个就不会很亮。而自发光,就是字面意思,自身是一个发光体。这里大概知道这些词是什么就可以,不必深究里面的原理。
这一篇博客,接下来我们将在Shader中实现一下漫反射。实现漫反射,可以在顶点函数中,这叫做逐顶点光照。也可以在片元函数中实现,这叫做逐片元光照。在顶点函数中实现,也就是对每一个顶点都进行一次光照的计算,而在片元函数中也就是对每一像素执行光照计算,所以,在片元函数中实现相对来说要更耗费一点性能。
在顶点函数中实现漫反射
漫反射的计算公式是 最终颜色=直射光颜色 * max(0, dot(光线,法线)),也就是使用 Directional Light 的颜色 乘 光线发射方向 与顶点法线方向的夹角,dot函数就是点乘,结果就是夹角。有一点要注意的是,dot中的 光线 和 法线 都是单位向量,也就是我们要对其进行标准化。max函数是取最大值,也就是说,如果dot计算出来的结果小于0,那就取0。
看下面的代码,注意看注释,从上往下每一个注释都要看
Shader "iMoeGirl/04-DiffuseVertex" {
SubShader {
Pass {
// 要使用光照,首先要定义一下LightMode,这里我们使用ForwardBase,
// 这里先不用管意思,只要照着写上就行
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
// 这里我们将 Unity 一些预定义的Shader代码包含进来,
// 里面有我们需要的东西,场景中第一个Directional Light的信息(后面用来做计算)
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
// 根据共识,要计算最终顶点的颜色,需要法线数据,所以这里将法线从Application传到顶点处理函数中
struct a2v {
float4 vertex: POSITION;
float3 normal: NORMAL; // NORMAL就是法线语义,之前的文章说过
};
struct v2f {
float4 position: SV_POSITION;
fixed3 color : COLOR; // 这个颜色就是在顶点函数中计算完的顶点的漫反射颜色,传到片元函数中
};
// 把光照的计算放在顶点函数中,所以叫做顶点光照
v2f vert(a2v v) {
// 定义一个数据传送结构体(传送到片元函数中)
v2f f;
f.position = UnityObjectToClipPos(v.vertex);
// 使用Unity内置变量 _WorldSpaceLightPos0 取到第0个直射光的方向,用于在cos中做计算
// 对于平行光来说,光的位置就是光的方向(在世界空间)
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
// 将法线转换到世界空间,然后标准化
// 因为灯光是在世界空间,所以后面的dot需要在一个空间中做计算
// 也可以将灯光转换到模型空间,只要保证在一个空间中即可
fixed3 normalInWorld = normalize(UnityObjectToWorldNormal(v.normal));
// 漫反射 = 真射光颜色 * max(0, dot(光的方向,法线))
// 使用Unity内置变量 _LightColor0.rgb 取到直射光的颜色
float3 diffuse = _LightColor0.rgb * max(0, dot(lightDir, normalInWorld));
// 将计算好的颜色传到片元函数中
f.color = diffuse;
return f;
}
float4 frag(v2f f) : SV_TARGET {
// 使用顶点函数中计算出来的颜色值的 rgb 作为最终颜色的返回值,alpha通道取1。
return fixed4(f.color, 1);
}
ENDCG
}
}
Fallback "VertexLit"
}
以上就是在顶点函数中计算最终的颜色,只要知道了光照模型,也就是知道了计算公式,就可以实现出来。简单来理解一下,就是当光照在一个点上时,这个点应该是什么颜色,要计算这个最终的颜色,有一个公式,只要把公式需要的参数都套进去,那最终的颜色就出来了。
在片元函数中实现漫反射
我们在上面的顶点函数中实现了漫反射的计算,因为顶点的数量远远小于像素的数据,所以在顶点中计算光照的颜色,会有一个问题,就是明暗交接出很不平滑,所以接下来我们将漫反射的计算放到片元函数中,然后再对比一下结果。
Shader "iMoeGirl/06-DiffuseFragment" {
SubShader {
Pass {
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#include "Lighting.cginc" // 取得第一个直射光的颜色 _LightColor0(Unity内置变量)
#pragma vertex vert
#pragma fragment frag
struct a2v {
float4 vertex: POSITION;
float3 normal: NORMAL;
};
struct v2f {
float4 position: SV_POSITION;
// 因为我们需要在片元函数中计算漫反射,所以需要法线的值,
// 也就是需要从法线值顶点函数中传到片元函数中,这里使用COLOR语义描述
fixed3 worldNormalDir : COLOR;
};
// 把光照的计算放在顶点函数中,所以叫做顶点光照
v2f vert(a2v v) {
v2f f;
f.position = UnityObjectToClipPos(v.vertex);
// 将法线转换到世界空间,然后标准化
fixed3 normalInWorld = normalize(UnityObjectToWorldNormal(v.normal));
// 将标准化后的法线值传到片元函数中
f.worldNormalDir = normalInWorld;
return f;
}
float4 frag(v2f f) : SV_TARGET {
// 对于平行光来说,光的位置就是光的方向(在世界空间)
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
// 取到标准化后的法线值
fixed3 normalInWorld = f.worldNormalDir;
// 漫反射 = 真射光颜色 * max(0, dot(光和法线的夹角))
float3 diffuse = _LightColor0.rgb * max(0, dot(lightDir, normalInWorld));
fixed3 color = diffuse;
return fixed4(color, 1);
}
ENDCG
}
}
Fallback "VertexLit"
}
好了,上面就是在片元函数中做漫反射的计算,我们来看一下 逐顶点光照 和 逐片元光照 的区别
左边是在顶点函数中计算漫反射,右边是在片元函数中计算漫反射,可以清楚地看出,在顶点函数中计算时,明暗交界处很不平滑,而在片元函数中计算时,明暗交界就很平滑。
因为最终呈现在屏幕上的颜色,是使用插值来填充的,也就是如果左边一个顶点是红色,右边一个顶点是黄色,那两个顶点中间的颜色就是红和黄的融合。顶点数量比像素数量少很多,所以插值的结果也就没有在片元函数中计算的结果平滑。
下一接我们将继续写与灯光相关的 Shader。