漫反射光照是Unity中最基本最简单的光照模型,本篇将会介绍在片元着色器中实现反射效果,并会采用半兰伯特光照技术对其进行改进。
1. 逐顶点光照与逐像素光照
在Unity Shader中,我们可以有两个地方可以用来计算光照:在顶点着色器中计算,被称为逐顶点光照(per-vertex lighting);在片元着色器中计算,被称为逐像素光照(per-pixel lighting)。
在逐像素光照中,我们会以每个像素为基础,得到它的法线(可以是对顶点法线插值得到的,也可以是从法线纹理中采样得到的),然后进行光照模型的计算。而逐顶点光照会在每个顶点上计算光照,然后会在渲染图元内部进行线性插值,最后输出成像素颜色。由于顶点数目往往远小于像素数目,因此逐顶点光照的计算量往往要小于逐像素光照,性能较高;但由于逐顶点光照会在渲染图元内部对顶点颜色进行插值,这又会导致渲染图元内部的颜色总是暗于顶点处的最高颜色值,这在某些情况下会产生明显的棱角现象,使渲染效果下降。
2. 初始定义
为了控制材质的漫反射颜色,首先要在Shader的Properties语义块中声明一个Color类型的属性,并把它的初始值设为白色:
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
接下来在Pass语义块中,指定我们的光照模式:
Tags {
"LightMode" = "ForwardBase" // 指定光照模式
}
为了使用Unity内置的一些变量,如_LightColor0,还需要包含Unity的内置文件Lighting.cginc:
#include "Lighting.cginc"
为了在Shader中使用Properties语义块中声明的属性,我们需要定义一个和该属性类型相匹配的变量:
fixed4 _Diffuse;
然后定义顶点着色器的输入和输出结构体:
// 顶点着色器输入 结构体
struct a2v
{
float4 vertex: POSITION; // 顶点坐标
float3 normal: NORMAL; // 法线方向
float4 texcorrd: TEXCOORD0; // 模型的第一套纹理坐标
};
// 顶点着色器向片元着色器的输出 结构体
struct v2f
{
float4 pos: POSITION; // 顶点在裁剪空间中的位置信息
float3 worldNormal: TEXCOORD0; // 世界空间法线
};
3. 实现漫反射
首先我们要在顶点着色器函数中计算世界空间法线并输出到片元着色器:
// 将法线从模型空间转世界空间
outData.worldNormal = mul(appData.normal, (float3x3)unity_WorldToObject);
return outData;
然后在片元着色器中,通过法线和光源计算辐照度:
// 规范化世界空间法线
fixed3 worldNormal = normalize(inputData.worldNormal);
// 规范化世界空间光源
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// 计算辐照度
fixed E = saturate(dot(worldNormal, worldLightDir));
有了辐照度,再混合光照色和反射色计算出漫反射:
// 漫反射 = 光照色 * 反射色 * 辐照度
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * E;
最后加入环境光,计算出反射光:
// 获取环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 环境光 + 漫反射
fixed3 color = ambient + diffuse;
return fixed4(color, 1);
渲染效果如下:
4. 使用半兰伯特模型对代码进行改进
我们发现该漫反射渲染有一个问题:在光照无法到达的区域,模型的表面呈现为全黑,没有任何明暗变化,这会使模型的背光区域看起来就像一个平面一样,失去了模型细节表现,因此我们需要引入半兰伯特光照技术对其进行改进。
我们在片元着色器代码中,将原先计算漫反射的部分,修改代码如下:
// 半兰伯特模型辐照度
fixed halfLambertE = E * 0.5 + 0.5;
// 漫反射 = 光照色 * 反射色 * 半兰伯特模型辐照度
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambertE;
渲染效果如下:
不过需要注意的是,半兰伯特是没有任何物理依据的,它仅仅是一个视觉加强技术。