基本光照模型一共有四种。分别为环境光,自发光,漫反射,高光反射,我们先来实现漫反射
下图使用兰伯特模型进行漫反射光计算(并且只进行了漫反射光计算),正面对着光线方向的部分最亮,然后逐渐变淡,
兰伯特模型
反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比
(也就是使用我们刚刚的“辐射度”作为漫反射光的强度)
我们按书里的流程写一个顶点/片元着色器来实现漫反射
首先新建一个Unity Shader,把原有代码全部删除,然后给shader起个名字
Shader "Diffuse-Lambert" {
}
为Shader添加一个Properties 语义块,声明我们需要的属性。
Properties 语义块是材质和Unity Shader的桥梁,它包含了一系列属性,这些属性会出现在检查器窗口的材质面板中。【candycat】
Shader "Diffuse-Lambert" {
Properties {
//_Color 为属性标识符,我们会在稍后的Shader编写中使用这个名字
//"Color Tint" 为在检查器窗口中显示的属性名称
//Color 为属性的类型
//等号右边的是属性的默认值,在这里四个1代表为“白色”
_Color ("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
}
}
此时在Unity中随意新建一个材质,并把我们的Diffuse-Lambert着色器拖到材质上,就可以在检查器面板看到我们刚刚声明的属性了
每一个Unity Shader可以定义多个SubShader,但最少要有一个。当Unity需要加载这个UnityShader时,Unity会扫描所有的SubShader语义块,并选出当前显卡能够支持的第一个SubShader运行。【candycat】
Shader "RefShader" { //属性 Properties { } //显卡A使用的子着色器 SubShader { } //显卡B使用的子着色器 SubShader { } }
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
}
SubShader {
//如果支持的话,当前显卡会使用这个子着色器
//不支持就靠 Fallback了
}
}
SubShader中定义了一系列Pass以及可选的状态和标签,每个Pass定义了一次完整的渲染流程,所以我们应该尽量使用最小数目的Pass。
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
}
SubShader {
Pass {
//在这里会进行一次完整的渲染
}
}
}
在Pass中,我们设置标签LightMode为ForwardBase(向前渲染),这是为了Unity能够按向前渲染路径的方式为我们正确提供各个光照变量
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" } //向前渲染
}
}
}
我们使用 CGPROGRAM作为CG/HLSL语言的开始符,而ENDCG是结束符。
在CG开始后,我们先来申明vert函数(顶点着色器,逐顶点渲染)和frag函数(片元着色器,逐片元渲染)
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" } //向前渲染
CGPROGRAM //CG开始
#pragma vertex vert
#pragma fragment frag
//app to vert,顶点着色器的输入类型,获取当前顶点的信息(由程序给出的)
struct a2v {
};
//vert to frag,片元着色器的输入类型,获取当前片元的信息(由顶点着色器输出的数据再插值得到的)
struct v2f {
};
//顶点着色器,逐顶点运行,输入a2v是当前顶点的信息,输出v2f给片元着色器
v2f vert(a2v v) {
}
//片元着色器,逐片元运行,输入v2f是当前片元的信息,输出颜色
fixed4 frag(v2f i) : SV_Target {
}
ENDCG //CG结束
}
}
}
这个流水线大约是这样子的:
现在我们的a2v还没有任何字段,也就是说我们的顶点着色器(vert)什么输入都没有,让我们使用语义来给它增加一些数据。
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" } //向前渲染
CGPROGRAM //CG开始
#pragma vertex vert
#pragma fragment frag
struct a2v {
//POSITION语义告诉Unity,用模型空间中当前顶点的坐标填充vertex变量
float4 vertex : POSITION;
//NORMAL语义告诉Unity,用模型空间中当前顶点法的线方向填充normal变量
float3 normal : NORMAL;
};
struct v2f {
};
v2f vert(a2v v) {
}
fixed4 frag(v2f i) : SV_Target {
}
ENDCG //CG结束
}
}
}
接着给我们的v2f定义结构体,以确保我们在稍后的片元着色器中能够有足够的数据进行运算
我们回顾一下兰伯特模型:
反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比
也就是说,只要得到了片元的表面法线和光源方向,我们就能得到该片元漫反射光线的强度。
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" } //向前渲染
CGPROGRAM //CG开始
#pragma vertex vert
#pragma fragment frag
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
//SV_POSITION语义告诉Unity,pos里包含了顶点在裁剪空间中的位置信息
//这也是顶点着色器最重要的一个工作:将顶点坐标从模型空间转换到裁剪空间
float4 pos : SV_POSITION;
//TEXCOORD0语义表示worldNormal变量占用了TEXCOORD0插值寄存器
//每个插值寄存器可以存储4个浮点值(float)
float3 worldNormal : TEXCOORD0; //世界空间下的顶点法线向量
float3 worldLightDir : TEXCOORD1; //世界空间下的光源位置
};
v2f vert(a2v v) {
}
fixed4 frag(v2f i) : SV_Target {
}
ENDCG //CG结束
}
}
}
在正式开始计算之前,为了使用一些Unity内置的变量和函数,我们需要包含进内置文件 "UnityCG.cginc"
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc" //包含内置文件"UnityCG.cginc"
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldLightDir : TEXCOORD1;
};
v2f vert(a2v v) {
}
fixed4 frag(v2f i) : SV_Target {
}
ENDCG
}
}
}
而为了可以使用我们在Properties 语义块中定义的属性(那个_Color),我们需要在CG中对属性进行申明
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
fixed4 _Color; //这里的变量名需要和属性的名字完全一致
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldLightDir : TEXCOORD1;
};
v2f vert(a2v v) {
}
fixed4 frag(v2f i) : SV_Target {
}
ENDCG
}
}
}
好了,开始写我们的顶点着色器(vert函数)。首先申明一个v2f类型的变量,对结构体中的字段依次赋值,最后将其返回。
v2f vert(a2v v) {
//申明返回值v2f
v2f o;
//这是顶点着色器最重要的一个任务,将顶点坐标从模型空间转换到裁剪空间
//UnityObjectToClipPos函数接受一个模型空间的坐标,返回该坐标在裁剪空间的坐标
o.pos = UnityObjectToClipPos(v.vertex);
//UnityObjectToWorldNormal函数接受一个模型空间的法线向量,将其转换到世界空间中并返回
o.worldNormal = UnityObjectToWorldNormal(v.normal);
//WorldSpaceLightDir函数接受一个模型空间中的顶点位置,并返回世界空间中从该点到光源的光照方向。未被归一化。(由于是平行光,任何点的光照方向都是一样的,参数填fixed(0)都可以)
o.worldLightDir = WorldSpaceLightDir(v.vertex);
return o;
}
经过顶点着色器的处理,我们的每个片元中已经包含我们需要的两个信息:法线向量和光源方向。现在,让我们在片元着色器中为每个片元计算他们的光线强度
由于我们稍后要用点积来求得两向量夹角的cos值,而点积的公式是a·b=|a||b|cosθ(θ为a和b的夹角),很明显,只有当a和b均为单位向量时,点积的结果才会是我们要的两向量的cos值。
fixed4 frag(v2f i) : SV_Target
{
//使用normalize()函数对向量进行归一化
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(i.worldLightDir);
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(i.worldLightDir);
//saturate()函数可以将值截取到[0,1],而dot()函数用于计算两向量间的点积
//计算出光线强度后,和我们的_Color属性相乘,就能实现一个亮度更改了
fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color;
//返回计算完成的颜色
return fixed4(diffuse, 1.0);
}
刚刚的计算我们实际上忽略了灯光的颜色,现在我们获取灯光颜色并加入计算。
为了获取灯光颜色,我们需要先在CG中包含头文件 "Lighting.cginc"
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc" //额外包含一个"Lighting.cginc"
#include "UnityCG.cginc"
fixed4 _Color;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldLightDir : TEXCOORD1;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldLightDir = WorldSpaceLightDir(v.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(i.worldLightDir);
fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color;
return fixed4(diffuse, 1.0);
}
ENDCG
}
}
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(i.worldLightDir);
//直接乘上灯光颜色 _LightColor0
fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color * _LightColor0;
return fixed4(diffuse, 1.0);
}
在兰伯特模型中,光照无法到达的区域,模型的外观通常是全黑的,没有任何明暗变化,这会使模型的背光区域看起来就像一个平面一样,失去了模型细节表现。【candycat】
Valve公司在开发《半条命》时提出了一个新技术,被称为半兰伯特光照模型。在半兰伯特模型中,我们不将片元上法线向量和光源方向的cos值截取到[0, 1],而是为这个cos值乘上α倍的缩放然后加上一个β大小的位移,通常,α和β都是0.5。也就是说,半兰伯特模型将法线向量和光源方向的cos值从[-1, 1]映射到了[0, 1]。
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(i.worldLightDir);
//兰伯特模型
//fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color * _LightColor0;
//半兰伯特模型
fixed3 diffuse = (dot(worldNormal, worldLightDir) * 0.5 + 0.5) * _Color* _LightColor0;
return fixed4(diffuse, 1.0);
}