Unityshader 标准光照模型(一)理论基础

1. 简介

虽然光照模型有很多种类,但在早期的游戏引擎中往往只使用一个光照模型,这个模型被称为标准光照模型。

在1975年,著名学者裴祥风(Bui Tuong Phong) 提出了标准光照模型背后的基本理念。

标准光照模型只关心**直接光照(directlight)**,也就是那些直接从光源发射出来照射到物体表面后,经过物体表面的一次反射直接进入摄像机的光线。

它的基本方法是,把进入到摄像机内的光线分为4个部分,每个部分使用一种方法来计算它的贡献度。

这4个部分是:

  • 自发光 (emissive)
    这个部分用于描述当给定一个方向时,一个表面本身会向该方向发射多少辐射量。
    需要注意的是:
    如果没有使用全局光照(globalillumination)技术,这些自发光的表面并不会真的照亮周围的物体,而是它本身看起来更亮了而已。

  • 高光反射(specular)
    这个部分用于描述当光线从光源照射到模型表面时,该表面会在完全镜面反射方向散射多少辐射量。

  • 漫反射(diffuse)
    这个部分用于描述,当光线从光源照射到模型表面时,该表面会向每个方向散射多少辐射量。

  • 环境光 (ambient)
    它用于描述其他所有的间接光照。

标准光照模型仅仅是一个经验模型,它并不完全符合真实世界中的光照现象。
但由于它的易用性、计算速度和得到的效果都比较好,因此仍然被广泛使用。

但这种模型有很多局限性。

  • 有很多重要的物理现象无法用Blinn-Phong模型表现出来,
    例如**菲涅耳反射(Fresnel reflection)**。
  • Blinn-Phong 模型各项同性(isotropic)
    当我们固定视角和光源方向旋转这个表面时,反射不会发生任何改变。
    但有些表面是具有**各向异性(anisotropic)**反射性质的例如拉丝金属、毛发等。
    所以它看起来不够真实

2. 理论基础

2.1 环境光

虽然标准光照模型的重点在于描述直接光照,但在真实的世界中,物体也可以被间接光照(indirect light)所照亮。

间接光照指:光线通常会在多个物体之间反射,最后进入摄像机。
也就是说,在光线进入摄像机之前,经过了不止一次的物体反射。
例如:
在红地毯上放置一个浅灰色的沙发,那么沙发底部也会有红色,这些红色是由红地毯反射了一部分光线,再反弹到沙发上的。

在标准光照模型中,我们使用了一种被称为环境光的部分来近似模拟间接光照。
环境光的计算非常简单,它通常是一个 全局变量,即场景中的所有物体都使用这个环境光。

环境光计算公式:
$c_{ambient}=g_{ambient}$
物体环境光=全局环境光

2.2 自发光

光线也可以直接由光源发射进入摄像机,而不需要经过任何物体的反射。

标准光照模型使用自发光来计算这个部分的贡献度。
它的计算也很简单,就是直接使用了该材质的自发光颜色:

自发光计算公式:
$c_{emissive}=m_{emissive}$
物体自发光=该物体材质自发光

通常在实时渲染中,自发光的表面往往并不会照亮周围的表面
也就是说,这个物体并不会被当成一个光源。
Unity5引入的全新的全局光照系统则可以模拟这类自发光物体对周围物体的影响

2.3 漫反射

漫反射光照是用于对那些被物体表面随机散射到各个方向的辐射度进行建模的。

在漫反射中,视角的位置是不重要的,因为反射是完全随机的,因此可以认为在任何反射方向上的分布都是一样的。但是,入射光线的角度很重要。

漫反射光照符合兰伯特定律(Lambert’s law):
反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比。

漫反射计算公式:
$c_{diffuse}=(c_{light}·m_{diffuse})max(0,n·I)$
物体漫反射光=(光源颜色·物体材质漫反射颜色)·取大值(0,法线方向矢量·光源方向矢量)
其中:

  • n是表面法线

  • I是指向光源的单位矢量

  • $m_{diffuse}$是材质的漫反射颜色

  • $c_{light}$ 是光源颜色。

需要注意的是:
我们需要防止法线和光源方向点乘的结果为负值,
为此,我们使用取最大值的函数来将其截取到0,
这可以防止物体被从后面来的光源照亮。

2.4 高光反射

2.4.1 Phong模型

这里的高光反射是一种经验模型, 也就是说,它并不完全符合真实世界中的高光反射现象。

它可用于计算那些沿着完全镜面反射方向被反射的光线,这可以让物体看起来是有光泽的,例如金属材质。

计算高光反射需要知道的信息比较多,如表面法线、视角方向、光源方向、反射方向等。
我们假设这些矢量都是单位矢量。如下图所示:
image-20210622112428355

在这四个矢量中,我们实际上只需要知道其中3个矢量即可,而反射方向可以通过其他信息计算得到:
$\hat r=2(\hat n·I)\hat n-I$

这样,我们就可以利用Phong 模型来计算高光反射的部分:
高光反射计算公式(Phong模型):
$c_{spscular}=(c_{light}·m_{specular})max(0,\hat v·\hat r)^{m_{gloss}} $
物体高光反射=(光源颜色·物体材质高光反射颜色)·取大值(0,视角向量方向·反射向量方向)物体光泽度次幕
其中:

  • $m_{specular}$是材质的光泽度(gloss), 也被称为反光度(shininess)。
    它用于控制高光区域的亮点”有多宽,mgloss越大,亮点就越小。

  • $m_{specular}$是材质的高光反射颜色,
    它用于控制该材质对于高光反射的强度和颜色。

  • $c_{light}$ 则是光源的颜色和强度。

同样,这里也需要防止$\hat v·\hat r$的结果为负数。

2.4.2 Blinn模型

和上述的Phong 模型相比,Blinn 提出了一个简单的修改方法来得到类似的效果。
它的基本思想是,避免计算反射方向$\hat r$。
为此,Blin 模型引入了一个新的矢量$\hat h$,它是通过对$\hat v$和$\hat I$的取平均后再归一化得到的。

即:
$\hat h=\frac {\hat v+I}{|\hat v+I|}$
然后,使用$\hat h$和$\hat n$之间的夹角进行计算,而非$\hat v$和$\hat r$之间的夹角如下图所示:
image-20210622114447454
高光反射计算公式(Blinn模型):
$c_{spscular}=(c_{light}·m_{specular})max(0,\hat n·\hat h)^{m_{gloss}} $
物体高光反射=(光源颜色·物体材质高光反射颜色)·取大值(0,法线方向·向量h方向)物体光泽度次幕

$\hat h=\frac {\hat v+I}{|\hat v+I|}$
$向量h方向=\frac {视角方向向量+光源方向向量}{|视角方向向量+光源方向向量|}$

在硬件实现时:

  • 若摄像机和光源距离模型足够远
    Blinn 模型会快于Phong模型
    因为此时可以认为$\hat v$和$\hat I$都是定值,因此$\hat h$将是一个常量。
  • 当$\hat v$和$\hat I$不是定值时,Phong 模型可能反而更快一些。

注意:
这两种光照模型都是经验模型
也就是说,我们不应该认为Blinn 模型是对“正确的”Phong模型的近似。
实际上,在一些情况下,Blinn 模型更符合实验结果。

2.5 标准光照模型的两种着色

计算光照模型通有两种选择:

  • 在片元着色器中计算——逐像素光照(per-pixel lighting)
    也被称为Phong着色(Phong shading)**或Phong插值法线插值**着色技术
    在逐像素光照中,我们会以每个像素为基础,得到它的法线
    (可以是对顶点法线插值得到的,也可以是从法线纹理中采样得到的),然后进行光照模型的计算。

    这不同于我们之前讲到的Phong光照模型。

  • 在顶点着色器中计算——逐顶点光照(per-vertex lighting)
    也被称为**高洛德着色(Gouraud shading)**。
    我们在每个顶点上计算光照,然后会在渲染图元内部进行线性插值,最后输出成像素颜色。
    由于顶点数目往往远小于像素数目,因此逐顶点光照的计算量往往要小于逐像素光照。

    注意:
    由于逐顶点光照依赖于线性插值来得到像素光照
    因此,当光照模型中有非线性的计算( 例如计算高光反射时)时,逐顶点光照就会出问题。
    而且,由于逐顶点光照会在渲染图元内部对顶点颜色进行插值,
    这会导致渲染图元内部的颜色总是暗于顶点处的最高颜色值,这在某些情况下会产生明显的棱角现象。

3.动手实践

3.1 在Unity中实现环境光与自发光

在标准光照模型中,环境光和自发光的计算是最简单的。
在Unity中,场景中的环境光可以在:
Window->Lighting -> Ambient Source/Ambient Color/Ambient Intensity(英文)
窗口->渲染->光照->场景->环境中控制,

如下图所示:
image-20210622131750865

在Shader中,我们只需要通过Unity的内置变量UNITY_ LIGHTMODEL AMBIENT就可以得到环境光的颜色和强度信息。

而大多数物体是没有自发光特性的。
如果要计算自发光也非常简单,我们只需要在片元着色器输出最后的颜色之前,把材质的自发光颜色添加到输出颜色上即可。

3.2 在UnityShader中实现漫反射光照模型

从公式可以看出,要计算漫反射需要知道4个参数:入射光线的颜色和强度Cight, 材质的漫
反射系数mdij
lifuse
表面法线介 以及光源方向I。2
为了防止点积结果为负值,我们需要使用max操作,而CG提供了这样的函数。在本例中,
使用CG的另一个函数可以达到同样的目的,即saturate函数。
函数: saturate(x)
参数: x:为用于操作的标量或矢量,可以是float、 float2、 float3 等类型。
描述:把x截取在[0, 1]范围内,如果x是一一个矢量,那么会对它的每一一个分量进行这样的操作。

3.2.1 逐项点光照

我们首先来看如何实现-一个逐顶点的漫反射光照效果。在学习完本节后,我们会得到类似
图6.6中的效果。
为此,我们进行如下准备工作。
(1)在Unity中新建一一个场景。在本书资源中,该场景名为Scene_ 6 _4。 在Unity 5.2中,默
认情况下场景将包含一个摄像机和一个平行光,并且使用了内置的天空盒子。在Window ->

Lighting -> Skybox中去掉场景中的天空盒子。
(2) 新建一个材质。在本书资源中,该材质名为DiffuseVertexLevelMato
(3)新建一一个 Unity Shader.在本书资源中,该Shader
名为Chapter6-DiffuseVertexLevel。 把新的Shader 赋给
第2步中创建的材质。
(4)在场景中创建一个胶囊体,并把第2步中的材
质赋给该胶囊体。
(5)保存场景。
下面,我们需要编写自己的Shader来实现–个逐顶
点的漫反射效果。打开第3步中创建的Unity Shader,
删除所有已有代码,并进行如下修改。
(1)首先,我们需要为这个Shader起一 一个名字:
Shader “Unity Shaders Book/Chapter 6/Dlffuse Vertex-Level” {
(2)为了得到并且控制材质的漫反射颜色,我们首先在Shader的Properties语义块中声明了
个Color类型的属性,并把它的初始值设为白色:
Properties {.
Diffuse (“Diffuse”, Color) = (1, 1, 1, 1)
(3)然后,我们在SsubShader语义块中定义了一个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
语义,一些资料中会使用TEXCOORDO语义。
(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, (f1oat3x3)_ Wor1d20bject));
// Get the 1ight 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;
在第一行,我们首先定义了返回值0。我们已经重复过很多次,顶点着色器最基本的任务就
是把顶点位置从模型空间转换到裁剪空间中,因此我们需要使用Unity内置的模型世界投影矩
阵UNITY MATRIX MVP来完成这样的坐标变换。接下来,我们通过Unity 的内置变量
UNITY LIGHTMODEL_ AMBIENT得到了环境光部分。
然后,就是真正计算漫反射光照的部分。回忆一下,为了计算漫反射光照我们需要知道4个
参数。在前面的步骤中,我们已经知道了材质的漫反射颜色Diffuse 以及顶点法线v.normal。我们
还需要知道光源的颜色和强度信息以及光源方向。Unity 提供给我们一“个内置变量LightColor0 来
访问该Pass 处理的光源的颜色和强度信息(注意,想要得到正确的值需要定义合适的LightMode
标签),而光源方向可以由
WorldSpaceLightPos0 来得到。需要注意的是,这里对光源方向的计算
并不具有通用性。在本节中,我们假设场景中只有一个光源且该光源的类型是平行光。但如果场
景中有多个光源并且类型可能是点光源等其他类型,直接使用_ WorldSpaceLightPos0就不能得到
正确的结果。我们将在6.6节中学习如何使用内置函数来处理更复杂的光源类型。
在计算法线和光源方向之间的点积时,我们需要选择它们所在的坐标系,只有两者处于同一
坐标空间下,它们的点积才有意义。在这里,我们选择了世界坐标空间。而由a2v得到的顶点法
线是位于模型空间下的,因此我们首先需要把法线转换到世界空间中。在4.7节中,我们已经知

3.2.2 逐像素光照