基础光照上篇主要讲的是unity中的光照模型及其原理,还有几种光照类型(自发光、环境光、漫反射、高光反射),后面几篇文章就开始在unity中实现这几种光照类型,本篇在unity实现漫反射。
下面我们来实现光照模型中的漫反射光照部分,计算公式如下:
基本光照模型中漫反射部分的计算公式:
Cdiffuse=(Clight · mdiffuse)∗max(0,n^ · l^)
要计算漫反射需要4个参数:
Clight = 入射光线的颜色和强度
mdiffuse = 材质的漫反射系数
n^ = 表面法线
l^ = 光源方向
为了防止点积结果为负值,我们需要使用max操作,而Cg提供了这样的函数。在本例中使用Cg的另一个函数可以达到统一的目的,即saturate函数。
函数:saturate(x)
参数x用于操作的标量或矢量,可以是float、float2、float3等类型。把x截取在[0,1]范围内,如果x是一个矢量。那么会对它的每一个分量进行这样的操作。
我们看一下如何实现一个逐顶点的漫反射光照效果。
1.为了得到并控制材质漫反射颜色,首先在shader中声明属性漫反射颜色,设置初始值白色:
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
2.然后在subshader语句块定义了一个pass,指明光照模式:
Pass {
Tags { "LightMode"="ForwardBase" }
LightMode标签是Pass标签中的一种,它用于定义该Pass在unity光照流水线的角色,定义了正确的LightMode,才能得到一些untiy的内置光照变量,例如后面要说的_LightColor0。
3.声明指令,包含内置文件Light.cginc,定义变量:
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
4.通过这样的方式,就可以得到漫反射属性,颜色属性的范围在0到1之间,可以使用fixed精度变量存储。
5.定义顶点着色器的输入和输出结构体(输出结构体同时也是片元函数的输入结构体)
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR;
};
6.漫反射部分咋顶点着色器进行:
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// Transform the normal from object space to world space
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
// Get the light direction in world space
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
o.color = ambient + diffuse;
return o;
}
首先定义返回值o,顶点着色器最基本任务是把顶点位置从模型空间转换到裁剪空间,接下来通过unity内置变量得到了环境光部分。
然后就是真正的计算漫反射光照部分,计算漫反射光照需要知道4个参数。前面已经知道了材质漫反射颜色_Diffuse以及顶点法线v.normal。还需要知道光源颜色和强度信息和光源方向。unity提供给我们了一个内置变量_LightColor0来访问该Pass处理的光源的颜色和强度信息(要得到正确的值需要定义合适的LightMode标签),而光源方向可以由_WorldSpaceLightPos来得到,注意这里对光源方向的计算不具有通用性,本节我们假设场景只有一个光源且是平行光。如果有多个光源并且类型可能是点光源等其他类型,直接使用它不能得到正确结果。
在计算法线和光源方向之间的点积时,我们需要选择他们所在的坐标系,只有两者处于同一坐标系点积才有意义,这里选择世界坐标空间。也要把顶点法线从模型转到世界空间,可以使用顶点变换矩阵的逆转置矩阵对法线进行相同变换,因此先得到模型空间到世界空间的变化矩阵的逆矩阵_World2Object,然后通过调用它在mul函数中位置,得到和转置矩阵相同的矩阵乘法。由于法线是一个三维矢量,因此只需截取前三行前三列即可。在得到世界空间法线和光源方向后,需要进行归一化操作。得到他们的点积结果后,需要防止这个结果为负值,为此使用了saturate函数,把参数范围截取到[0,1]之间,最后再与光源颜色和强度以及漫反射颜色相乘得到最终漫反射光照部分。
最后对环境光和漫反射部分相加,得到最终光照结果。
7.由于所有计算在顶点着色器完成了,片元着色器之间把顶点颜色输出即可:
fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}
效果图:
至此详细解释了逐顶点漫反射光照的实现,对于细分程度较高的模型,逐顶点光照以及可以取得比较好的光照效果了,但对于一些细分程度低的模型,逐顶点光照会出现一些视觉问题,比如背光面和向光面交界处有一些锯齿,为了解决视觉问题我们可以使用逐像素的漫反射光照。
只需要对shader进行一些更改就可以使用逐像素的漫反射效果了。
1.修改上节的顶点着色器输出结构图v2f:
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
};
2.顶点着色器不需要计算光照模型,只需把世界空间下的法线传递给片元着色器即可:
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = UnityObjectToClipPos(v.vertex);
// Transform the normal from object space to world space
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
return o;
}
3.片元着色器需要计算漫反射光照模型:
fixed4 frag(v2f i) : SV_Target {
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// Get the normal in world space
fixed3 worldNormal = normalize(i.worldNormal);
// Get the light direction in world space
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 color = ambient + diffuse;
return fixed4(color, 1.0);
}
效果图:
逐像素光照可以得到更加平滑的光照效果,但是即使使用了逐像素漫反射光照,有一个问题仍然存在。在光照无法到达的区域,模型外观通常是全黑的,没有任何明暗变化,这会使得模型背光区域看起来就像一个平面一样,失去了模型细节表现。实际上可以通过添加环境光来得到非全黑效果,但是仍然无法解决背光面明暗一样的缺点,于是有一种改善技术被提出来,叫半兰伯特光照模型。
上面我们使用的漫反射光照模型也被称为兰伯特光照模型,因为它符合兰伯特定律——在平面某点漫反射光的光强与该反射点的法向量和入射光角度的余弦值成正比。为了改变上一小节提出的问题,在原兰伯特光照模型上做了一个简单的修改,因此叫半兰伯特光照模型。
广义的半兰伯特光照模型的公式如下:
Cdiffuse=(Clight · mdiffuse)∗(α(n^ · l^)+β)
可以看出,半兰伯特光照模型没有使用max操作防止点积为负值,而是对其结果进行了一个α倍的缩放再加上一个β大小的偏移。绝大多数情况下,α和β的值均为0.5,即公式为:
Cdiffuse=(Clight · mdiffuse)∗(0.5(n^ · l^)+0.5)
通过这样的方式,我们可以把点积的结果范围从[1-,1]映射到[0,1]范围内。也就是说对于模型的背光面,在原兰伯特光照模型中点积结果将映射到同一个值,即0处;而在半兰伯特光照模型中,背光面也可以有明暗变化,不同的点积结果会映射到不同的值上。
半兰伯特是没有任何物理依据的,它仅仅是一个视觉加强技术。
下面修改上一小节写的代码,片元着色器中计算漫反射的部分:
fixed4 frag(v2f i) : SV_Target {
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// Get the normal in world space
fixed3 worldNormal = normalize(i.worldNormal);
// Get the light direction in world space
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);
}
下图给出了逐顶点漫反射光照、逐像素漫反射光照、半兰伯特光照的对比效果: