unity入门精要之第6 章 Unity 中的基础光照--环境光和自发光

Unity系列文章目录

前言

但这种模型有很多局限性。首先,有很多重要的物理现象无法用Blinn-Phong 模型表现出来,
例如菲涅耳反射(Fresnel reflection)。其次,Blinn-Phong 模型是各项同性(isotropic)的,也就
是说,当我们固定视角和光源方向旋转这个表面时,反射不会发生任何改变。但有些表面是具有
各向异性(anisotropic)反射性质的,例如拉丝金属、毛发等。在第18 章中,我们将学习基于物
理的光照模型,这些光照模型更加复杂,同时也可以更加真实地反映光和物体的交互。

一、Unity 中的环境光和自发光

在标准光照模型中,环境光和自发光的计算
是最简单的。
在Unity 中,场景中的环境光可以在Window ->
Lighting -> Ambient Source/Ambient Color/Ambient
Intensity 中控制,如图6.5 所示。在Shader 中,我
们只需要通过Unity 的内置变量UNITY_LIGHTM
ODEL_AMBIENT 就可以得到环境光的颜色和强
度信息。
而大多数物体是没有自发光特性的,因此在
本书绝大部分的Shader 中都没有计算自发光部
分。如果要计算自发光也非常简单,我们只需要
在片元着色器输出最后的颜色之前,把材质的自
发光颜色添加到输出颜色上即可。

在这里插入图片描述

二、在Unity Shader 中实现漫反射光照模型

在了解了上述的理论后,我们现在来看一下如何在Unity 中实现这些基本光照模型。首先,
我们来实现标准光照模型中的漫反射光照部分。
在6.2.3 节中,我们给出了基本光照模型中漫反射部分的计算公式:
从公式可以看出,要计算漫反射需要知道4 个参数:入射光线的颜色和强度clight,材质的漫
反射系数mdiffuse,表面法线n 以及光源方向l 。
为了防止点积结果为负值,我们需要使用max 操作,而Cg 提供了这样的函数。在本例中,
使用Cg 的另一个函数可以达到同样的目的,即saturate 函数。
函数:saturate(x)
参数:x:为用于操作的标量或矢量,可以是float、float2、float3 等类型。
描述:把x 截取在[0, 1]范围内,如果x 是一个矢量,那么会对它的每一个分量进行这样的操作。
6.4.1 实践:逐顶点光照
我们首先来看如何实现一个逐顶点的漫反射光照效果。在学习完本节后,我们会得到类似
图6.6 中的效果。
为此,我们进行如下准备工作。
(1)在Unity 中新建一个场景。在本书资源中,该场景名为Scene_6_4。在Unity 5.2 中,默
认情况下场景将包含一个摄像机和一个平行光,并且使用了内置的天空盒子。在Window ->
Lighting -> Skybox 中去掉场景中的天空盒子。
(2)新建一个材质。在本书资源中,该材质名为DiffuseVertexLevelMat。
(3)新建一个Unity Shader。在本书资源中,该Shader
名为Chapter6-DiffuseVertexLevel。把新的Shader 赋给
第2 步中创建的材质。
(4)在场景中创建一个胶囊体,并把第2 步中的材
质赋给该胶囊体。
(5)保存场景。
下面,我们需要编写自己的Shader 来实现一个逐顶
点的漫反射效果。打开第3 步中创建的Unity Shader,
删除所有已有代码,并进行如下修改。
(1)首先,我们需要为这个Shader 起一个名字:
Shader “Unity Shaders Book/Chapter 6/Diffuse Vertex-Level” {
(2)为了得到并且控制材质的漫反射颜色,我们首先在Shader 的Properties 语义块中声明了
一个Color 类型的属性,并把它的初始值设为白色:
Properties {
_Diffuse (“Diffuse”, Color) = (1, 1, 1, 1)
}
(3)然后,我们在SubShader 语义块中定义了一个Pass 语义块。这是因为顶点/片元着色器的
代码需要写在Pass 语义块,而非SubShader 语义块中。而且,我们在Pass 的第一行指明了该Pass
的光照模式:
SubShader {
Pass {
Tags { “LightMode”=“ForwardBase” }
LightMode 标签是Pass 标签中的一种,它用于定义该Pass 在Unity 的光照流水线中的角色,
在第9 章中我们会更加详细地解释它。在这里,我们只需要知道,只有定义了正确的LightMode,
我们才能得到一些Unity 的内置光照变量,例如下面要讲到的_LightColor0。
(4)然后,我们使用CGPROGRAM 和ENDCG 来包围Cg 代码片,以定义最重要的顶点着色
器和片元着色器代码。首先,我们使用#pragma 指令来告诉Unity,我们定义的顶点着色器和片元
着色器叫什么名字。在本例中,它们的名字分别是vert 和frag:
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
(5)为了使用Unity 内置的一些变量,如后面要讲到的_LightColor0,还需要包含进Unity 的
内置文件Lighting.cginc:
#include “Lighting.cginc”
(6)为了在Shader 中使用Properties 语义块中声明的属性,我们需要定义一个和该属性类型
相匹配的变量:
fixed4 _Diffuse;
通过这样的方式,我们就可以得到漫反射公式中需要的参数之一—材质的漫反射属性。由
于颜色属性的范围在0 到1 之间,因此我们可以使用fixed 精度的变量来存储它。
在这里插入图片描述
(7)然后,我们定义了顶点着色器的输入和输出结构体(输出结构体同时也是片元着色器的
输入结构体):
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR;
};
为了访问顶点的法线,我们需要在a2v 中定义一个normal 变量,并通过使用NORMAL 语义
来告诉Unity 要把模型顶点的法线信息存储到normal 变量中。为了把在顶点着色器中计算得到的
光照颜色传递给片元着色器,我们需要在v2f 中定义一个color 变量,且并不是必须使用COLOR
语义,一些资料中会使用TEXCOORD0 语义。
(8)接下来是关键的顶点着色器。由于本小节关注如何实现一个逐顶点的漫反射光照,因此
漫反射部分的计算都将在顶点着色器中进行:
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 fram object space to world space
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));
// 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 内置的模型世界投影矩
阵UNITY_MATRIX_MVP 来完成这样的坐标变换。接下来,我们通过Unity 的内置变量
UNITY_LIGHTMODEL_AMBIENT 得到了环境光部分。
然后,就是真正计算漫反射光照的部分。回忆一下,为了计算漫反射光照我们需要知道4 个
参数。在前面的步骤中,我们已经知道了材质的漫反射颜色_Diffuse 以及顶点法线v.normal。我们
还需要知道光源的颜色和强度信息以及光源方向。Unity 提供给我们一个内置变量_LightColor0 来
访问该Pass 处理的光源的颜色和强度信息(注意,想要得到正确的值需要定义合适的LightMode
标签),而光源方向可以由_WorldSpaceLightPos0 来得到。需要注意的是,这里对光源方向的计算
并不具有通用性。在本节中,我们假设场景中只有一个光源且该光源的类型是平行光。但如果场
景中有多个光源并且类型可能是点光源等其他类型,直接使用_WorldSpaceLightPos0 就不能得到
正确的结果。我们将在6.6 节中学习如何使用内置函数来处理更复杂的光源类型。
在计算法线和光源方向之间的点积时,我们需要选择它们所在的坐标系,只有两者处于同一
坐标空间下,它们的点积才有意义。在这里,我们选择了世界坐标空间。而由a2v 得到的顶点法
线是位于模型空间下的,因此我们首先需要把法线转换到世界空间中。在4.7 节中,我们已经知
道可以使用顶点变换矩阵的逆转置矩阵对法线进行相同的变换,因此我们首先得到模型空间到世
界空间的变换矩阵的逆矩阵_World2Object,然后通过调换它在mul 函数中的位置,得到和转置矩
阵相同的矩阵乘法。由于法线是一个三维矢量,因此我们只需要截取_World2Object 的前三行前三
列即可。
在得到了世界空间中的法线和光源方向后,我们需要对它们进行归一化操作。在得到它们点
积的结果后,我们需要防止这个结果为负值。为此,我们使用了saturate 函数。saturate 函数是Cg
提供的一种函数,它的作用是可以把参数截取到[0, 1]的范围内。最后,再与光源的颜色和强度以
及材质的漫反射颜色相乘即可得到最终的漫反射光照部分。
最后,我们对环境光和漫反射光部分相加,得到最终的光照结果。
(9)由于所有的计算在顶点着色器中都已经完成了,因此片元着色器的代码很简单,我们只
需要直接把顶点颜色输出即可:
fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}
(10)最后,我们需要把这个Unity Shader 的回调shader 设置为内置的Diffuse:
Fallback “Diffuse”
至此,我们已经详细解释了逐顶点的漫反射光照的实现。对于细分程度较高的模型,逐顶点
光照已经可以得到比较好的光照效果了。但对于一些细分程度较低的模型,逐顶点光照就会出现
一些视觉问题,例如我们可以在图6.6 中看到在胶囊体的背光面与向光面交界处有一些锯齿。为
了解决这些问题,我们可以使用逐像素的漫反射光照。
6.4.2 实践:逐像素光照
我们只需要对Shader 进行一些更改就可以实现逐像素的漫反射效果,如图6.7 所示。

在这里插入图片描述
为此,我们进行如下准备工作。
(1)使用6.4.1 节中使用的场景。
(2)新建一个材质。在本书资源中,该材质名为DiffusePixelLevelMat。
(3)新建一个Unity Shader。在本书资源中,该Shader 名为Chapter6-DiffusePixelLevel。把新
的Shader 赋给第2 步中创建的材质。
(4)把第2 步中创建的材质赋给胶囊体。

Chapter6-DiffusePixelLevel 的代码和6.4.1 小节中的非常相似,因此我们首先把6.4.1 节中的
代码直接粘贴到Chapter6-DiffusePixelLevel 中,并进行如下修改。
(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 = mul(UNITY_MATRIX_MVP, v.vertex);
// Transform the normal fram object space to world space
o.worldNormal = mul(v.normal, (float3x3)_World2Object);
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);
}
上面的计算过程和6.4.1 节完全相同,这里不再赘述。
逐像素光照可以得到更加平滑的光照效果。但是,即便使用了逐像素漫反射光照,有一个问
题仍然存在。在光照无法到达的区域,模型的外观通常是全黑的,没有任何明暗变化,这会使模
型的背光区域看起来就像一个平面一样,失去了模型细节表现。实际上我们可以通过添加环境光
来得到非全黑的效果,但即便这样仍然无法解决背光面明暗一样的缺点。为此,有一种改善技术
被提出来,这就是半兰伯特(Half Lambert)光照模型。
6.4.3 半兰伯特模型
在6.4.1 小节中,我们使用的漫反射光照模型也被称为兰伯特光照模型,因为它符合兰伯特定
律—在平面某点漫反射光的光强与该反射点的法向量和入射光角度的余弦值成正比。为了改善
6.4.2 小节最后提出的问题,Valve 公司在开发游戏《半条命》时提出了一种技术,由于该技术是
在原兰伯特光照模型的基础上进行了一个简单的修改,因此被称为半兰伯特光照模型。
广义的半兰伯特光照模型的公式如下:
可以看出,与原兰伯特模型相比,半兰伯特光照模型没有使用max 操作来防止n 和l 的点积
为负值,而是对其结果进行了一个倍的缩放再加上一个大小的偏移。绝大多数情况下,和
的值均为0.5,即公式为:
通过这样的方式,我们可以把n · l 的结果范围从[−1, 1]映射到[0, 1]范围内。也就是说,对于
模型的背光面,在原兰伯特光照模型中点积结果将映射到同一个值,即0 值处;而在半兰伯特模
型中,背光面也可以有明暗变化,不同的点积结果会映射到不同的值上。
需要注意的是,半兰伯特是没有任何物理依据的,它仅仅是一个视觉加强技术。
对6.4.2 小节中得到的代码做一些修改就可以实现半兰伯特漫反射光照效果。
(1)仍然使用6.4.1 小节中使用的场景。
(2)新建一个材质。在本书资源中,该材质名为HalfLambertMat。
(3)新建一个Unity Shader。在本书资源中,该Shader 名为Chapter6-HalfLambert。把新的
Shader 赋给第2 步中创建的材质。
(4)把第2 步中创建的材质赋给胶囊体。
打开Chapter6-HalfLambert,删除已有的Shader 代码,把6.4.2小节的Chapter6-DiffusePixelLevel
代码粘贴进去,并使用半兰伯特公式修改片元着色器中计算漫反射光照的部分:
fixed4 frag(v2f i) : SV_Target {

// 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);
}
在上面的代码中,我们使用半兰伯特模型代替了原有的兰伯特模型。图6.8 给出了逐顶点漫
反射光照、逐像素漫反射光照和半兰伯特光照的对比效果。

在这里插入图片描述

参考

Unity Shader入门精要
作者:冯乐乐

猜你喜欢

转载自blog.csdn.net/aoxuestudy/article/details/124370069