目录
一、基础光照模型(The Basic Lighting Model)
(3)Diffuse(Lambert / Half-Lambert)
(4)Specular(Phong / Blinn-Phong)
1、逐顶点光照模型(Per-Vertex Lighting)
2、逐像素光照模型(Per-Fragment Lighting)
本文分别介绍了两种漫反射光照模型(Lambert和Half-lambert)、两种镜面反射模型(Phong和Blinn-phong)、自发光、环境光的理论基础和它们在unity中的计算方式。并将这些光照分别在逐顶点光照模型中和逐像素光照模型中进行了完整的计算。并附完整代码和展示渲染的结果。
一、基础光照模型(The Basic Lighting Model)
在基础光照模型中,物体的表面颜色是自发光(The Emissive Term)、环境光(The Ambient Term)、漫反射(The Diffuse Term)和镜面反射(The Specular Term)的总和。每种光照项都取决于表面的材料属性(例如亮度和材质颜色)和光源属性(例如光的颜色和位置)的组合。将每个光照项都表示为一个三维的float3或half3的向量。 这个三维的向量包含了RGB三个颜色值,分别为红色、绿色、蓝色。用数学描述这个方程式的基本模型: surfaceColor = emissive + ambient + diffuse + specular 。下面我们分别了解一下每项的光照基础以及计算过程。
1、光照理论
(1)自发光项(The Emissive Term)
自发光项(Emissive Term)表示的是表面发出的光,它是独立于所有光源的。自发光就是一个RGB值,这个发光的物体本身不是光源,它不会照亮其他物体,也不会投下阴影。如图1说明了自发光项的概念。单独计算这个渲染结果是比较枯燥的,因为它对于整个物体来说只有一个颜色。自发光项是在计算完所有其他的光照之后添加的颜色。(更先进的全局照明模型会模拟发射的光如何影响场景的其余部分,本篇暂时不涉及此知识点)
(图1)
以下是自发光项的数学公式:
emissive = Ke |
Ke :是材质的自发光颜色
(2)环境光项(The Ambient Term)
环境光项(The Ambient Term)指的是场景中环境光反射的光线。环境光是来自四面八方的,它几乎到达每一个角落。 环境光的方向也是不确定的。 因此,环境照明项不依赖于光源位置。 图2说明了这个概念,图2显示了一个只接收环境光的物体的渲染。 环境项取决于材质的环境反射率,以及入射到材质上的环境光的颜色。 就像辐射项一样,环境光项本身就是一个恒定的颜色。 但与自发光项不同的是,环境光项会受全局环境照明的影响。
(图2)
下面是环境光项的数学公式:
ambient = Ka x globalAmbient |
Ka:是材质的环境光颜色,或者说环境反射率
globalAmbient:是入射环境光的颜色
(3)漫反射项(The Diffuse Term)
漫反射项(The Diffuse Term)是指从一个表面向各个方向均匀反射的定向光。 一般来说,漫反射表面在微观尺度上是粗糙的,有小的角落和缝隙,可以向多个方向反射光线。 当光线照射到这些角落和缝隙时,光线会向四面八方反射,如图3-1所示。
(图3-1)
反射光的量与照射在表面上的光线的入射角成正比。 特点是表面比较暗淡,比如布满灰尘的黑板,是具备漫反射属性的。 漫反射的特性是,无论视方向在哪里,表面上任何特定点的漫反射贡献都是相同的。 如图3-2所示。
(图3-2)
下面是漫反射项的数学公式:(如图3-3所示)
diffuse = Kd x lightColor x max(0, N · L) |
Kd:是物体表面的漫反射颜色
lightColor:是入射漫射光的颜色
N:是归一化曲面法线
L:为指向光源的归一化矢量
P:是被遮蔽的点
(图3-3)
(4)镜面反射项(The Specular Term)
镜面反射项(The Specular Term)表示光主要围绕镜面方向从表面散射。镜面反射项在非常光滑和有光泽的表面上显示最为突出,例如抛光的金属、塑料材质等。 图4展示了镜面反射的概念。
( 图4)
它与自发光、环境光、漫反射光照不同,镜面反射的贡献取决于观看者的位置。 如果观察者不在接收反射光线的位置,就不会在表面上看到镜面高光。 镜面反射项不仅受光源和材质的镜面颜色特性的影响,还受表面的光泽程度的影响,即shininess 。 较亮的材质有更小、更紧的高光,而比较不亮的材料有更分散的高光。 如图5所示,亮度的指数从左到右递增。
如图5不同的亮度指数示例
下面是镜面反射项的数学公式:(如图6所示)
specular = Ks x lightColor x facing x (max(N · H, 0)) shininess |
Ks:是材质的镜面颜色
lightColor:是入射光线的颜色
N:是归一化曲面法线,
V:是视角方向的向量(需归一化)
L:是指向光源方向(需归一化)
H:是V和L相加得到的向量(需归一化)
P:是被遮蔽的点,
facing :如果N dot L大于0,则朝向正面,值为1;否则朝向负面,值为0。
(图6)
视方向V 和半角向量H之间的夹角越小时,材质的镜面反射的效果显示就会越明显;当V和H之间的夹角越大时,镜面反射效果就会越弱。 这是由法线向量N和H的点积的幂的结果决定了,
另外,如果漫射项为零,则镜面反射项会强制变为零,这是因为N 点积 L(来自漫反射的照明)为负。这确保了镜面反射高光不会出现在远离光线的几何体上。
最后,将所有项相加(Adding the Terms Together):表面颜色 = 自发光项 + 环境光项 + 漫反射项 + 镜面反射项
2、光照计算
(1)Emission
在我们物理世界中,多数的物体都没有自发光特性,所以它一般属于特效的范围里。自发光项贡献在光照计算中是最简单的。我们可以直接创建一个名为 emissive 的三维变量来表示自发光的贡献。
// 计算自发光项
half3 emissive = Ke;
(2)Ambient
对于环境光项的贡献,我们需要拿到材质的环境光颜色Ka,并把它的颜色分量,分别乘以全局环境光颜色的分量。如下所示:
float3 ambient;
ambient.x = Ka.x * globalAmbient.x;
ambient.y = Ka.y * globalAmbient.y;
ambient.z = Ka.z * globalAmbient.z;
上边这段代码确实是可以工作的,但是这样方法是比较低效的。我们可以直接适用一个三维的向量来表示它。并且Unity在它内置函数中直接提供了这个变量,我们可以在Shader中直接调取使用。使用内置的变量和矩阵等来计算是很方便的,下面分别是在BuildIn管线中和URP管线中的调取结果,在URP管线中它的名称有所改变。如下所示:
//BuilIn-管线,环境光项的贡献
half3 ambient = _GlossyEnvironmentColor.xyz;
//URP-管线,环境光项的贡献
half3ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
(3)Diffuse(Lambert / Half-Lambert)
漫反射的光照模型常见有两种,一种是兰伯特(Lambert),另一种是半兰伯特(Half-Lambert)。半兰伯特是在兰伯特的基础上做了稍加改进。其中,Unity为我们提供了可以直接调取的内置的灯光颜色的变量_LightColor0。_LightColor0是Unity内置管线的灯光颜色变量名称,在URP管线内,灯光颜色的变量名被改为_MainLightColor。
兰伯特光照模型(Lambert)
我们上边说的 漫反射项(The Diffuse Term)的光照模型也被称为兰伯特光照模型(Lambert),因为它符合兰伯特定律——在平面某点漫反射光的光强与该反射点的法向量和入射光角度的余弦值成正比。
从漫反射公式diffuse = Kd x lightColor x max(0, N · L)中看到,我们需要4个参数,物体表面的漫反射颜色Kd、入射光线的颜色和强度lightColor、 表面法线向量N、光源方向的向量L。
为了避免结果为负值,我们使用了Unity内置的max函数,把法线向量(N)点积光源方向(L)的结果限制在0到1的范围。颜色的属性在 0 到 1 之间,因此我们使用 half 精度的变量来储存输出的颜色值(diffuse)就够用了
//BulidIn管线
half3 diffuse = diffuseColor * _LightColor0.rgb * max(0.0, dot(N, L));//URP管线
half3 diffuse = diffuseColor * _MainLightColor.rgb * max(0.0, dot(N, L));
半兰伯特光照模型(Half-Lambert)
半兰伯特(Half-Lambert)是Valve公司在开发游戏《半条命》时提出的一种技术。它是在兰伯特(Lambert)的基础上进行了一个简单的修改,因此被称为半兰伯特光照模型(Half-Lambert Lighting Mode)。
广义的半兰伯特光照模型公式如下:
diffuse = (Kd x lightColor) x (α(N · L) + β)
与兰伯特模型相比,半兰伯特模型没有使用max函数来操作防止法线向量(N)和光方向(L)的点积为负值,而是对其结果进行了一个α倍的缩放再加上一个β大小的偏移。绝大多数情况下,α和β的值均为0.5,即公式为:
diffuse = (Kd x lightColor) x (0.5(N · L) + 0.5) |
通过这样的方式,我们可以直接把(N · L)的结果从[-1,1]映射到[0,1]范围内,不需要使用max函数来做限制了。对于模型的背光面,在原兰伯特光照模型中点击结果将映射到同一个值,即0值处。而在半兰伯特模型中,背光面也可以有明暗变化,不同的点击结果会映射到不同的值上。
(图7)
需要注意的是,半兰伯特是没有任何物理依据的,它仅仅是一个视觉加强技术。计算方法如下:
//BuildIn管线
half3 diffuse = diffuseColor * _LightColor0.rgb * max(0.0, dot(N, L)) * 0.5 + 0.5;//URP管线
half3 diffuse = diffuseColor * _MainLightColor.rgb * max(0.0, dot(N, L)) + 0.5 * 0.5;
兰伯特和半兰伯特,点积的曲线图。如下图8所示:
(图8)
(4)Specular(Phong / Blinn-Phong)
常见的镜面反射的光照模型有两种,一种是Phong光照模型,另一种是Blinn-phong光照模型。Blinn-phong模型是在Phong模型的基础上做了稍加改进。
Phong 光照模型
从镜面反射公式中 镜面反射项(The Specular Term)可以看到,镜面反射需要拿到4个参数,分别是材质的镜面颜色Ks 、入射光线的颜色和强度lightColor、视角方向的向量V、反射方向的向量R。
Cg/Hlsl为我们提供了计算反射方向的函数reflect。当给定表面法线(N)和入射方向(I)时,reflect函数可以返回反射方向,图9给出了参数和返回值之间的关系:
(图9)
反射向量是一个三维的向量,这里的入射方向(I)指的是入射光的光方向。计算如下:
half3 R= reflect(I, N)
Blinn-Phong光照模型
Blinn-phong没有引入反射向量,而是引入了一个新的矢量-半角向量H。它是通过对视方向(V)和 光方向(I)相加后再归一化得到的。 图10给出了参数和返回值之间的关系:
(图10)
其中视角方向,是由世界空间的相机位置减去世界空间的顶点坐标位置 ,而世界空间的相机位置Unity直接提供给我们了。它们的计算如下计所示:
//视角方向
V = normalize( _WorldSpaceCameraPos.xyz - worldPos );
// 半角向量
H = normalize(V + I);
// 法线点乘半角向量,并且限制在0到1的范围。
NdotH = dot(N, H);
Phong 和 Blinn-Phong的区别
Phong和Blinn-phong都属于经验模型,只是Phong模型要比Blinn-phong模型的计算量大,主要因为反射向量reflect的计算量较大。而且Blinn-phong模型计算出的高光更柔和,要比Phong的结果更真实一些。所以,当Blinn-phong模型推出之后,几乎就替代了Phong模型。
二、Unity中实现光照模型
在shader中我们在计算光照时,有两种渲染方式。一种是逐顶点光照(Per-Vertex Lighting),另一种是逐像素光照(Per-Fragment Lighting)。
逐顶点光照的优点是非常的节省性能,因为所有的光照计算几乎都在顶点着色器中完成。但是缺点是渲染效果非常的差。它的渲染质量,很大一部分程度是由模型顶点数量决定的,所以称之为逐顶点光照。但是在游戏中模型的顶点数量是有一定限制的。如果模型的顶点数量过低,渲染的结果就会显示非常大的像素块或这类似折痕的感觉。在早期的低端机上,由于硬件的限制会使用逐顶点的渲染形式较多。随着移动设备的硬件逐渐提高,逐顶点光照模型几乎已经被弃用。
逐像素光照渲染,它的所有光照计算都是在片元着色器中进行的,所以无论模型的面数高低,它的渲染结果都是要比逐顶点光照模型要好非常非常多。显而易见,它的优点是最终的效果非常细腻非常的好。但是由于是逐像素渲染,所以它要比逐顶点光照模型耗费非常非常的大。它们完全不是一个量级的。由于目前硬件设备不断提高,所以完全可以承担它的计算。因此逐像素光照模型目前几乎完美的替代了逐顶点光照,成为了当今的主流。
下面我们分别来看一下它们的渲染过程,以及最终的结果展示:
1、逐顶点光照模型(Per-Vertex Lighting)
在逐顶点光照计算中:漫反射的计算我们使用的是 兰伯特光照模型(Lambert)、镜面反射的计算使用的是Phong 光照模型。
(1)计算过程
- 首先为shader起个名字:
Shader"Example/Vertex Lighting"
- 为控制材质漫反射颜色_DiffuseColor 和 镜面反射颜色_SpecularColor,在shader的Properties语义块中给它们分别声明一个Color类的属性,并把它们的初始值值设为白色。_Gloss用于控制高光区域的大小。
Properties
{
_DiffuseColor("Diffuse Color",color) = (1,1,1,1)
_SpecularColor("Specular Color",color) = (1,1,1,1)
_Gloss("Gloss", Range(8.0, 265)) = 20
}
- 在Subshadr代码块中定义一个Pass语义块。顶点着色器和片元着色器的代码块需要写在pass里,在pass的第一行指明该pass的光照模式。(LightMode标签是Pass标签中的一种,只有定义了正确的LightMode,我们才能得到一些Unity的内置光照变量。例如下边说到的_LightColor0.rgb。)
Subshader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
- 使用CGPROGRAM 和 ENDCG 来包围代码片,来定义顶点着色器和片元着色器代码。首先使#pragma指令告诉Unity所定义的顶点着色器和片元着色器叫什么名字。我们把它们分别取名为 vert 和 frag :
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
- 为了使用Unity的内置变量,需要包含内置文件 UnityCG.cginc :
#include "UnityCG.cginc"
- 为了在Shader计算中使用 Properties 语义块里声明的属性,需要定义和属性类型相匹配的变量,通过这样的方式,就可以得到漫反射公式中的物体表面的漫反射颜色信息(Kd)、镜面反射公式中材质的镜面颜色(Ks)。Unity给我们提供了一个内置变量 _LightColor0 来访问该Pass处理的光源颜色和强度信息,我们需要在这里定义这个变量(需要注意的是,想要得到正确的值需要定义合适的LightMode标签)。
颜色的属性在 0 到 1 之间,因此我们使用half精度的变量来储存它就够用了。
half4 _DiffuseColor;
half4 _SpecularColor;
float _Gloss;
half4 _LightColor0;
- 下面定义了顶点着色器的输入和输出结构体(顶点着色器的输出结构体,同时也是片元着色器的输入结构体)
为了访问顶点法线,需要在a2v中定义一个normal变量,通过使用NORMAL语义把模型顶点的法线信息储存到normal变量中。
为了把顶点着色器中的光照计算传给片元,需要在v2f中定义一个color变量。
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
half3 color : COLOR;
};
- 接下来开始在顶点着色器中实现逐顶点的光照的计算,因此漫反射光照和镜面反射光照部分会在顶点着色器中进行。
其中光源方向可以由 _WorldSpaceLightPos0 来得到,需要注意的是_WorldSpaceLightPos0是获取平行光的光方向。
在Unity-shader中光源方向的计算并不具备通用性,unity中的光源有平行光、点光源、聚光灯。
如果我们用此shader来使用平行光以外的光源,对于其它光源(点光源、聚光灯)来说可能得不到正确的结果。
v2f vert (appdata v)
{
v2f o;
// 把顶点从模型空间变换到剪裁空间
o.vertex = UnityObjectToClipPos(v.vertex);
// 获取unity内置的环境光项的宏变量
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 把法线从模型空间变换到世界空间
half3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
// 获取世界空间的光方向
half3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// 计算漫反射项 (Lambert)
half3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * saturate(dot(worldNormal, worldLight));
// 获取世界空间中的反射向量
half3 reflectDir = normalize(reflect(-worldLight, worldNormal));
// 获取视方向
half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_WorldToObject, v.vertex).xyz);
// 计算漫反射光照模型(Phong光照模型)
half3 specular = _SpecularColor * _LightColor0.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
// 最后把环境光、把漫反射 和镜面反射 相加的结果存储到最后的颜色中
o.color = ambient + diffuse + specular;
return o;
}
- 最后把顶点着色器中光照的计算的结果传输到片元着色器中,并输出。至此完成了逐顶点的光照的计算。
half3 frag (v2f i) : SV_Target
{
half3 col = i.color;
return half4(col,1);
}
(2)完整代码
Shader "Example/Vertex Lighting"
{
Properties
{
_DiffuseColor("Diffuse Color",color) = (1,1,1,1)
_SpecularColor("Specular Color",color) = (1,1,1,1)
_Gloss("Gloss", Range(8.0, 265)) = 20
}
SubShader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
half3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
half3 color : COLOR;
};
half4 _DiffuseColor;
half4 _SpecularColor;
float _Gloss;
half4 _LightColor0;
v2f vert (appdata v)
{
v2f o;
// 把顶点从模型空间变换到剪裁空间
o.vertex = UnityObjectToClipPos(v.vertex);
// 获取unity内置的环境光项的宏变量
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 把法线从模型空间变换到世界空间
half3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
// 获取世界空间的光方向
half3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// 计算漫反射项 (Lambert)
half3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * saturate(dot(worldNormal, worldLight));
// 获取世界空间中的反射向量
half3 reflectDir = normalize(reflect(-worldLight, worldNormal));
// 获取视方向
half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_WorldToObject, v.vertex).xyz);
// 计算漫反射光照模型(Phong光照模型)
half3 specular = _SpecularColor * _LightColor0.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
// 最后把环境光、把漫反射 和镜面反射 相加的结果存储到最后的颜色中
o.color = ambient + diffuse + specular;
return o;
}
half3 frag (v2f i) : SV_Target
{
half3 col = i.color;
return half4(col,1);
}
ENDCG
}
}
}
(3)渲染结果
我们可以看到最终的效果图11所示是有明显的棱角,逐顶点光照渲染的结果并不细腻。包括它的明暗交接线处,有很大的色块过度非常的不均匀。就是因为它是逐顶点的计算。在图12展示了此模型的顶点分布。
图11的效果,调节了材质球上漫反射的颜色、镜面反射颜色、环境光颜色。其中环境光的颜色在Unity-Lighting面板下,Environment选项中的 Ambient Color里来调节。
(图11)
逐像素光照计算的结果依托于模型的顶点数量,模型的顶点数量越高,渲染的结果就越细腻,越好。此球体的模型顶点的分布如图12所示:
(图12)
2、逐像素光照模型(Per-Fragment Lighting)
在逐像素光照计算中:漫反射的计算我们使用的是半兰伯特光照模型(Half-Lambert);镜面反射使的计算使用的是Blinn-Phong光照模型。
(1)计算过程
- 复制一份逐顶点光照shader(完整代码),修改Shader名字:
Shader"Example/Vertex Lighting"
- 首先修改顶点着色器的输出结构体 v2f,这里我们不需要在v2f结构体中定义color变量了,因为所有的光照计算会在片元着色其中直接计算。但有些数据必须在顶点着色器中计算,然后再传输到片元着色器中进行使用。比如下边结构体中定义的世界空间的顶点坐标(worldPos)、世界空间的法线数据(worldNormal)。
所以我们需要在结构体 v2f 中定义 worldPos 和worldNormal 这两个变量,用来传输它们。如下所示:
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 worldPos : TEXCOORD0;
half3 worldNormal : TEXCOORD1;
};
- 在顶点着色器中的计算如下:
v2f vert (appdata v)
{
v2f o;
// 把顶点从模型空间变换到剪裁空间
o.vertex = UnityObjectToClipPos(v.vertex);
// 获取世界空间的顶点坐标位置
o.worldPos = mul(unity_WorldToObject, v.vertex).xyz;
// 把法线从模型空间变换到世界空间
o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
return o;
}
- 最后我们开始在片元着色器中进行计算光照。从顶点着色其中传输过来法线的数据,在片元着色器中拿到光方向,并且把漫反射计算修改为半兰伯特(Half-Lambert)光照模型。在这里我们计算了半角向量(H),然后把镜面反射的计算修改为Blinn-phong光照模型。最后直接在片元着色器中输出了光照的结果。
至此我们完成了逐片元光照的计算。计算如下所示:
half3 frag (v2f i) : SV_Target
{
//从顶点着色器中拿到法线数据
half3 worldNormal = normalize(i.worldNormal);
// 获取unity内置的环境光项的宏变量
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 获取世界空间的光方向
half3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// 计算漫反射项 (Half-lambert)
half3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * (saturate(dot(worldNormal, worldLight))*0.5 +0.5 );
// 获取世界空间中的反射向量
half3 reflectDir = normalize(reflect(-worldLight, worldNormal));
// 获取视方向
half3 viewDir = normalize(_WorldSpaceCameraPos.xyz -i.worldPos);
// 计算半角向量
half3 H = normalize(viewDir + worldLight);
// 计算漫反射光照模型(Blinn-Phong光照模型)
half3 specular = _SpecularColor * _LightColor0.rgb * pow(saturate(dot(worldNormal, H)), _Gloss);
// 最后把环境光、把漫反射 和镜面反射 相加的结果存储到最后的颜色中
half3 col = ambient + diffuse + specular;
return half4(col,1);
}
(2)完整代码
Shader "Example/Fragment Lighting"
{
Properties
{
_DiffuseColor("Diffuse Color",color) = (1,1,1,1)
_SpecularColor("Specular Color",color) = (1,1,1,1)
_Gloss("Gloss", Range(8.0, 265)) = 20
}
SubShader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
half3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 worldPos : COLOR;
half3 worldNormal : TEXCOORD0;
};
half4 _DiffuseColor;
half4 _SpecularColor;
float _Gloss;
half4 _LightColor0;
v2f vert (appdata v)
{
v2f o;
// 把顶点从模型空间变换到剪裁空间
o.vertex = UnityObjectToClipPos(v.vertex);
// 获取世界空间的顶点坐标位置
o.worldPos = mul(unity_WorldToObject, v.vertex).xyz;
// 把法线从模型空间变换到世界空间
o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
return o;
}
half3 frag (v2f i) : SV_Target
{
//从顶点着色器中拿到法线数据
half3 worldNormal = normalize(i.worldNormal);
// 获取unity内置的环境光项的宏变量
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 获取世界空间的光方向
half3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// 计算漫反射项 (Half-lambert)
half3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * (saturate(dot(worldNormal, worldLight))*0.5 +0.5 );
// 获取世界空间中的反射向量
half3 reflectDir = normalize(reflect(-worldLight, worldNormal));
// 获取视方向
half3 viewDir = normalize(_WorldSpaceCameraPos.xyz -i.worldPos);
// 计算半角向量
half3 H = normalize(viewDir + worldLight);
// 计算漫反射光照模型(Blinn-Phong光照模型)
half3 specular = _SpecularColor * _LightColor0.rgb * pow(saturate(dot(worldNormal, H)), _Gloss);
// 最后把环境光、把漫反射 和镜面反射 相加的结果存储到最后的颜色中
half3 col = ambient + diffuse + specular;
return half4(col,1);
}
ENDCG
}
}
}
(3)渲染结果
我们可以看到逐像素光照计算的最终结果如图13所示渲染效果是非常细腻的,没有任何的棱角感。图13中的球体和上边的逐像素光照中使用的球体是同样的模型顶点数量。
图13中的漫反射计算使用的是半兰伯特光照模型(Half-lambert),镜面反射我们使用的是Blinn-phong光照模型,它也属于经验模型。但看起来更符合实验效果。
最终的效果,调节了材质球上漫反射的颜色、镜面反射颜色、环境光颜色。 其中环境光的颜色在Unity-Lighting面板下,Environment选项中的 Ambient Color里来调节。
(图13)
三、渲染效果对比及介绍
1、Lambert和Half-lambert
我们使用逐像素光照框架来测试兰伯特(Lambert)和半兰伯特(Half-lambert)的效果对比,如图14中渲染结果是非常显而易见的, 半兰伯特更具有美感。它的暗部没有像兰伯特模型那样死黑的区域。而100%的黑色在美术中是比较慎用的颜色,绘画中几乎不会使用100%的黑色去画一个区域。但半兰伯特的效果在感官上是比较平的,就是立体感不够强。所以它不适合于立体感很强的美术风格中。
从渲染结果来看,兰伯特光照模型比较适合偏写实风或者暗黑风格,而半兰伯特光照模型非常适合偏卡通渲染风格,它几乎霸占卡渲领域。
( 图14)
2、Phong和Blinn-phong
我们使用逐像素光照框架来测试 Phong光照模型 和Blinn-phong光照模型 的效果对比。如图15中的Blinn-phong模型的高光点会更圆更饱满,更接近真实。而Phong模型的高光点是有一点趋于椭圆的形状。之前有说到Blinn-phong的计算量要少于 Phong的计算量。所以我们更多的会使用Blinn-phong光照模型。
( 图15)
3、逐顶点光照和逐像素光照
如下图16中,左边图是逐顶点光照渲染的效果,里面的光照计算使用的是 Lambert光照模型 + Phong光照模型 对应的shader是(点这里:完整代码);右边图是逐像素光照渲染的效果,里面的光照计算使用的是 Half-lambert光照模型 + Blinn-phong光照模型,对应的shader是(点这里:完整代码)。
( 图16)