Unity3D基础篇----Shader学习笔记(3)

时间:2021-03-17 04:08:40

这一篇,我们来系统的介绍一下关于Unity的光照模型。

我们先来看看我们这一篇所实现的效果:

Unity3D基础篇----Shader学习笔记(3)

分别实现了半Lambert光照模型、Phong氏光照模型以及Blinn-Phong光照模型。

在讲光照模型之前,我们先来对上一篇中关于兰伯特光照模型进行一下补充。

这里先给出上节的实现效果图:

Unity3D基础篇----Shader学习笔记(3)

不管是逐顶点还是逐像素的兰伯特光照模型,向光面虽然是亮的,但是背光面有些是暗得看不见了。往往这不是我们需要的,我们通常看到暗部的时候,还是依稀能够看清物体的。这个原因是因为计算Lambert值时限定了它的值,即根据光的方向和物体表面法线进行点积,因为都为方向向量,所以主要取决于点积中两向量的cos值,而cos的取值范围为[-1,1]之间,而我们在取兰伯特值是使用了max来限定该值的,小于0则使用0:

float Lambert = max(dot(normalDir, lightDir), 0);//兰伯特值

所以,当两个角度大于有180°的时候便没有值了,所以物体显现为黑色。

这里,我们需要对它进行改进,我们通过使用这么一条公式来修改:

value = a*cos(角度)+b
我们对cos的值进行a倍缩放,然后再b偏移。一般,它们的值均为0.5.

所以,我们修改后的Lambert值如下:

float Lambert = 0.5 * dot(normalDir, lightDir) + 0.5;//兰伯特值

其它属性不变,这样,就可以得到一个比较亮的物体了,而且暗部基本还是能够看得清。

关于半Lambert的代码实现如下:

//Shader模块定义
Shader "xiaolezi/Half Lambert Lighting Model Shader"
{
//属性设置
Properties
{
//定义一个物体表面颜色,格式:[属性名]([Inspector面板显示名字],属性类型)=[初始值]
_DiffuseColor("Diffuse Color", Color) = (1, 1, 1, 1)
}
//第一个SubShader块
SubShader
{
//第一个Pass块
Pass
{
//指定灯光渲染模式
Tags{ "LightMode" = "ForwardBase" }

//开启CG着色器编辑模块
CGPROGRAM
//定义顶点着手器函数名
#pragma vertex vert
//定义片段着色器函数名
#pragma fragment frag

//包含相关头文件
#include "UnityCG.cginc"
#include "Lighting.cginc"

//定义一个从应用程序到顶点数据的结构体
struct appdata
{
float4 vertex : POSITION;//POSITION语义:表示从该模型中获取到顶点数据
float3 normal : NORMAL; //NORMAL语义:获取该模型法线
};
//定义一个从顶点数据到片段数据的结构体
struct v2f
{
float4 pos : SV_POSITION;//SV_POSITION语义:从顶点输出数据中获取到顶点数据
float3 normal : COLOR0;//COLOR0语义:定义法线变量
float4 vertex : COLOR1;//COLOR1语义:定义顶点变量
};
//从属性模块中取得该变量
fixed4 _DiffuseColor;
//顶点着色器函数实现
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);//让模型顶点数据坐标从本地坐标转化为屏幕剪裁坐标
o.normal = v.normal;
o.vertex = v.vertex;
return o;
}
//片段着色器函数实现
fixed4 frag(v2f f) : SV_Target//SV_Target语义:输出片元着色器值,可直接认为是输出到屏幕颜色
{
fixed3 normalDir = normalize(UnityObjectToWorldNormal(f.normal));//计算世界法线方向
fixed3 lightDir = normalize(ObjSpaceLightDir(f.vertex));//计算灯光方向

float Lambert = 0.5 * dot(normalDir, lightDir) + 0.5;//兰伯特值
fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * Lambert;//计算漫反色
return fixed4(diffuse, 1.0);
}
//结束CG着色器编辑模块
ENDCG
}
}
Fallback "Diffuse"//默认着色器
}

好,现在进入我们的正题,Unity中的光照模型。关于光与物体间的影响关系以及相关知识点,这里就不多讲解,我们这里就直接来说说满足光照模型所需要具备的条件:

1.自发光:光线可以直接由光源直接进入摄像机,而不需要经过其他物体的反射,它会直接取自材质的自发光颜色。通常来说,物体的自发光会影响周围物体,但是在没有使用全局光照的情况下,自发光是不被考虑的;

2.环境光:我们知道,物体表面除了受直接光照影响之外,周围物体对光照的反射或散射也会对物体产生的影响,为了模拟这一部分影响,我们直接使用Unity内置的环境光变量UNITY_LIGHTMODEL_AMBIENT来直接模拟环境光颜色,在Unity编辑器中,环境光的设置在菜单Windows->Lighting->Settings->Environment选项中可以进行相关设置;

3.漫反色:当光线从光源照射到物体模型表面时,会散射相对应幅度值,所以这里的漫反色计算便是上一篇中我们实现的Lambert光照模型。

4.高光反射:当光线从光源照射到物体模型表面时,该表面会在完全镜面反射方向散射多少幅度值。该值的计算我们使用这么一个公式:

最终高光值 = 灯光颜色 * 材质高光颜色 * 高光模型值^材质光泽度

其中,材质高光颜色用于控制该材质对于高光的强度和颜色。材质光泽度用于控制高光区域的范围大小,该值越大,范围越小。而高光模型值的计算有如下两种方式:

Phong氏高光值:摄像机的观察方向与光照方向在物体模型法线的反射向量方向的点积。

Blinn-Phong高光值:物体表面模型法线与摄像机方向和灯光方向的角平分线的点积。


现在来看看具体的实现,我们先来看看Phong氏光照模型的实现:

//Shader模块定义
Shader "xiaolezi/Phong Lighting Model Shader"
{
//属性设置
Properties
{
//定义一个物体表面颜色,格式:[属性名]([Inspector面板显示名字],属性类型)=[初始值]
_DiffuseColor("Diffuse Color", Color) = (1, 1, 1, 1)
_Glossness("Glossness", Range(8, 256)) = 20//物体光泽度
_SpecularColor("Specular Color", Color) = (1, 1, 1, 1)//高光颜色
}
//第一个SubShader块
SubShader
{
//第一个Pass块
Pass
{
//指定灯光渲染模式
Tags{ "LightMode" = "ForwardBase" }

//开启CG着色器编辑模块
CGPROGRAM
//定义顶点着手器函数名
#pragma vertex vert
//定义片段着色器函数名
#pragma fragment frag

//包含相关头文件
#include "UnityCG.cginc"
#include "Lighting.cginc"

//定义一个从应用程序到顶点数据的结构体
struct appdata
{
float4 vertex : POSITION;//POSITION语义:表示从该模型中获取到顶点数据
float3 normal : NORMAL; //NORMAL语义:获取该模型法线
};
//定义一个从顶点数据到片段数据的结构体
struct v2f
{
float4 pos : SV_POSITION;//SV_POSITION语义:从顶点输出数据中获取到顶点数据
float3 normal : COLOR0;//COLOR0语义:定义法线变量
float4 vertex : COLOR1;//COLOR1语义:定义顶点变量
};
//从属性模块中取得该变量
fixed4 _DiffuseColor;
float _Glossness;
fixed4 _SpecularColor;

//顶点着色器函数实现
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);//让模型顶点数据坐标从本地坐标转化为屏幕剪裁坐标
o.normal = v.normal;
o.vertex = v.vertex;
return o;
}
//片段着色器函数实现
fixed4 frag(v2f f) : SV_Target//SV_Target语义:输出片元着色器值,可直接认为是输出到屏幕颜色
{
fixed3 normalDir = normalize(UnityObjectToWorldNormal(f.normal));//计算世界法线方向
fixed3 lightDir = normalize(ObjSpaceLightDir(f.vertex));//计算灯光方向
fixed3 viewDir = normalize(ObjSpaceViewDir(f.vertex));//计算观察方向

//环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//漫反色
float Lambert = 0.5 * dot(normalDir, lightDir) + 0.5;//兰伯特值
fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * Lambert;//计算漫反色
//高光
fixed3 reflectDir = normalize(reflect(-lightDir, normalDir));//根据物体表面法线计算光的反射光方向
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(reflectDir, viewDir)), _Glossness);//Phong氏高光计算

return fixed4(ambient + diffuse + specular, 1.0);
}
//结束CG着色器编辑模块
ENDCG
}
}

Fallback "Specular"//默认着色器,这里选择高光
}
我们直接看到片元着色器函数的实现。

首先我们先定义了需要计算的向量方向,我们都通过模型坐标来转换为相对应的向量方向。漫反色需要使用的是法线和灯光向量方向,而高光需要使用法线、灯光以及摄像机向量方向:

fixed3 normalDir = normalize(UnityObjectToWorldNormal(f.normal));//计算世界法线方向
fixed3 lightDir = normalize(ObjSpaceLightDir(f.vertex));//计算灯光方向
fixed3 viewDir = normalize(ObjSpaceViewDir(f.vertex));//计算观察方向

环境光直接使用内置变量来取值:

//环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;


漫反色我们使用半兰伯特光照模型来实现:

//漫反色
float Lambert = 0.5 * dot(normalDir, lightDir) + 0.5;//兰伯特值
fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * Lambert;//计算漫反色


比较复杂的就是高光的计算:

//高光
fixed3 reflectDir = normalize(reflect(-lightDir, normalDir));//根据物体表面法线计算光的反射光方向
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(reflectDir, viewDir)), _Glossness);//Phong氏高光计算
首先我们通过reflect函数求出入射光根据物体表面法线所反射的向量,然后再通过该值求出高光模型值之后与灯光和材质高光颜色进行相乘得到最终高光颜色。

最后,再对所计算的颜色值进行叠加得到最终颜色值并返回输出到屏幕上:

return fixed4(ambient + diffuse + specular, 1.0);


如果明白了Phong氏高光模型,Blinn-Phong高光模型自然也就很容易懂:

//Shader模块定义
Shader "xiaolezi/Blinn Phong Lighting Model Shader"
{
//属性设置
Properties
{
//定义一个物体表面颜色,格式:[属性名]([Inspector面板显示名字],属性类型)=[初始值]
_DiffuseColor("Diffuse Color", Color) = (1, 1, 1, 1)
_Glossness("Glossness", Range(8, 256)) = 20
_SpecularColor("Specular Color", Color) = (1, 1, 1, 1)
}
//第一个SubShader块
SubShader
{
//第一个Pass块
Pass
{
//指定灯光渲染模式
Tags{ "LightMode" = "ForwardBase" }

//开启CG着色器编辑模块
CGPROGRAM
//定义顶点着手器函数名
#pragma vertex vert
//定义片段着色器函数名
#pragma fragment frag

//包含相关头文件
#include "UnityCG.cginc"
#include "Lighting.cginc"

//定义一个从应用程序到顶点数据的结构体
struct appdata
{
float4 vertex : POSITION;//POSITION语义:表示从该模型中获取到顶点数据
float3 normal : NORMAL; //NORMAL语义:获取该模型法线
};
//定义一个从顶点数据到片段数据的结构体
struct v2f
{
float4 pos : SV_POSITION;//SV_POSITION语义:从顶点输出数据中获取到顶点数据
float3 normal : COLOR0;//COLOR0语义:定义法线变量
float4 vertex : COLOR1;//COLOR1语义:定义顶点变量
};
//从属性模块中取得该变量
fixed4 _DiffuseColor;
float _Glossness;
fixed4 _SpecularColor;

//顶点着色器函数实现
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);//让模型顶点数据坐标从本地坐标转化为屏幕剪裁坐标
o.normal = v.normal;
o.vertex = v.vertex;
return o;
}
//片段着色器函数实现
fixed4 frag(v2f f) : SV_Target//SV_Target语义:输出片元着色器值,可直接认为是输出到屏幕颜色
{
fixed3 normalDir = normalize(UnityObjectToWorldNormal(f.normal));//计算世界法线方向
fixed3 lightDir = normalize(ObjSpaceLightDir(f.vertex));//计算灯光方向
fixed3 viewDir = normalize(ObjSpaceViewDir(f.vertex));//计算观察方向

//环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//漫反色
float Lambert = 0.5 * dot(normalDir, lightDir) + 0.5;//兰伯特值
fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * Lambert;//计算漫反色
//高光
fixed3 halfDir = normalize(lightDir + viewDir);//根据物体表面法线计算光的反射光方向
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(halfDir, normalDir)), _Glossness);//Phong氏高光计算

return fixed4(ambient + diffuse + specular, 1.0);
}
//结束CG着色器编辑模块
ENDCG
}
}

Fallback "Specular"//默认着色器,这里选择高光
}

主要区别便是高光的计算不同:

//高光
fixed3 halfDir = normalize(lightDir + viewDir);//根据物体表面法线计算光的反射光方向
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(halfDir, normalDir)), _Glossness);//Phong氏高光计算
具体的就不再多讲了。

关于这两个模型的高光计算,其实Blinn-Phong会相对Phong光照模型会好很多,比如上面使用reflect函数来计算反射向量,当中涉及的计算相对于这个求角平分线的要复杂很多,所以性能自然也会下降很多。就效果而言,Blinn-Phong光照模型会比Phong光照模型亮很多。所以,Blinn-Phong光照模型也是对Phong氏光照模型的一种拓展优化。


以上便是对Unity中光照模型进行简单的介绍以及实现,希望能够对读者有所启发。

代码仓库也已经更新,有需要的可以进入链接下载页面进行克隆下载:GitHub仓库地址

Happy Coding...