Unity 渲染原理(九)Unity光照模型

我们正式开始学习一些能够应用的Shader,这次我们就从光照模型开始。本文中的代码和概念解读自《Unity Shader 入门精要》.

本文主要讲解书中的几种光照模型的原理和代码分析。

现实世界的光照——我们是如何看到这个世界的

我们构建的游戏世界的目的,就是为了模仿一个现实的世界出来,所以我们要对现实世界的物体成像的原理进行数学建模,然后用程序来实现这个数学模型。

所以我们在学习Unity光照模型之前的第一步,就是对现实世界的光照有一个基本的了解。

我们在描述“这个物体是红色的”时候,实际上是因为这个物体会反射更多的红色光,而把其他的颜色的光波都吸收了,而一个物体时黑色的,其实是它吧所有颜色的光波都吸收了。

通常来讲,我们要模拟真实的光照来生成一张图像,需要考虑三种物理现象:

  • 首先,光线从光源中被发射出来
  • 然后,光线和场景中的一些物体相遇,一些光线被物体吸收,一些光线被物体散射到其他方向。
  • 最后,摄像机吸收了一些光,产生了一张图像

光源

光是由光源发射出来的。在实时渲染中,我们通常把光源看成一个没有体积的点,用l来表示它的方向。那么我们如何测量一个光源发射出了多少光呢?也就是说,我们如何量化一个光?在光学里面,这个叫做辐照度。对于平行光而言,它的辐照度可以通过计算垂直于l的单位面积内穿过的能量来得到。物体表面一般和l是不垂直的,在计算一个物体表面的辐照度的时候,我们可以通过计算光照方向l表面法线之间的夹角余弦值。

因为辐照度和照射到物体表面时光线之间的距离d/cosθ成反比,因此辐照度就和cosθ成正比。cosθ可以由光源方向l和表面法线n的点积得来。

吸收和散射

光线由光源发射出来以后,与物体相交。通常相交的结果有两种:散射和吸收。

散射只改变光线的方向,而不改变光线的密度和颜色。而吸收只会改变光线的密度和颜色,但是不改变光线的方向。

光线在物体表面经过散射后,有两种方向,一种将会散射到物体的内部,这种现象被称为折射或透射;另一种将会散射到外部,这种现象被称为反射。

对于不透明物体,折射进入物体内部的光线还可能继续与内部的颗粒相交,其中一些光线最后回重新发射出物体表面,另一些则被物体吸收。那些从物体表面重新发射出的光线将具有和入射光线不同的方向分布和颜色。

为了区分这两种不同的散射,我们在光照模型中使用不同的部分来计算它们:高光反射(specular)表示物体如何反射光线,而漫反射(diffuse)表示有多少光线会被折射,吸收和散射出表面。

根据入射光线的数量和方向,我们可以计算出射光线的数量和方向,我们通常使用出射度来描述它。

总结一下,光线与物体相交的结果有两种:散射和吸收,散射又分为折射和反射,反射我们用高光反射来计算,折射用漫反射来计算。

着色

着色(Shading)指的是,根据材质属性(如漫发射属性等)、光源信息(如光源方向,辐照度等),使用一个等式去计算沿着某个观察方向的出射度的过程,我们把这个等式称为光照模型(Lighting Mode),不同的光照模型有不同的目的,一些用语描述粗糙的表面,一些用于描述金属表面。

BRDF模型

当光线从某个方向照射到一个表面时,有多少光线被反射,反射方向有哪些?

BRDF(Bidirectional Reflection Distribution Function,双向反射分布函数)就是回答这个问题的。当给定模型表面上的一个点时,BRDF包含了对该点外观的完整描述。在图形学中,BRDF大多使用一个数学公式来表示,并且提供一个参数来调整材质属性。

标准光照模型

光照模型有很多种,但是在早期的游戏引擎中往往只使用一个光照模型,这个模型被称为标准光照模型。实际上,在BRDF提出之前,标准光照模型就被广泛使用了。

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

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

  • 自发光(emissive)部分。这个部分用于描述当给定一个方向时,一个表面会向该方向发射出多少辐射量。注意,如果没有使用全局光照(Global illumination),这些自发光的表面并不会真的照亮周围的物体,只是本身看起来更亮了而已。具体的详情可以看一下官方文档:发光材质 - Unity
  • 高光反射(Specular)部分。这个部分描述当光线从光源照射到模型表面时,该表面会在完全镜面反射方向散射多少辐射量。
  • 漫反射(diffuse)部分。这个部分用于描述,当光线从光源照射到模型表面时,该表面会向各个方向散射多少辐射量。
  • 环境光(ambient)部分。所有其他间接光照(相对于直接光照而言)。

环境光

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

间接光照指的是,光线会在多个物体之间反射,最后进入摄像机。例如在红地毯附近放一个浅灰色的沙发,那么沙发底部也会有红色,这个红色是因为地毯反射了一部分光线,再反弹到沙发上的。

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

cambient=gambientc_{ambient} = g_{ambient}

自发光

光线也可以直接由光源发射进入相机,而不需要经过任何物体的反射。标准光照模型使用自发光来计算这部分的贡献。它的计算公式也很简单,就是使用了该材质的自发光颜色。

cemissive=memissivec_{emissive} = m_{emissive}

通常在实时渲染时,自发光表面往往并不会照亮周围的物体,也就是这个物体并不会被当做一个光源。

漫反射

漫发射光照适用于对那些物体表面随机散射到各个方向的辐射度进行建模的。在漫发射中,视角的位置是不重要的,因为反射完全随机,因此可以认为在任何反射方向上的分布都是一样的,但是入射光线的角度很重要。

漫反射符合兰伯特定律(Lambert’s Law):反射光线的强度与表面发现和光源之间的夹角的余弦值成正比。因此漫反射部分的计算公式是:

cdiffuse=(clightmdiffuse)max(0,nI)c_{diffuse} = (c_{light} \cdot m_{diffuse})max(0, n \cdot I)

其中,n是表面法线,I是光源的单位矢量,mdiffusem_{diffuse}是材质的漫反射颜色,clightc_{light}是光源的颜色。
需要注意的是,为了防止法线和光源方向点乘结果为负值,为此,我们使用取最大值的函数来将其截取到0,这样可以防止物体被来自后面的光源照亮。

这里有个问题,为什么这里是两个颜色的点乘而不是相加,两个颜色值相乘是个什么结果?
https://www.jianshu.com/p/70f5e349cd49

高光反射

这里说的高光反射是一种经验模型,也就是说,它并不完全符合真实世界里的高光反射现象。它可用于计算那些沿着完全镜面反射方向被反射的光线,这可以让物体看起来有光泽,例如金属材质。

计算高光反射需要知道的信息比较多,如表面法线,视角方向,光源方向,反射方向等。

我们假设这些矢量都是单位矢量,而这4个矢量我们知道三个就可以,第四个——反射方向我们是可以计算的(原理可以参考:https://blog.csdn.net/a1191835397/article/details/102779766)

r=2(nI)nIr = 2(n \cdot I)n - I

这样我们就能用Phong模型计算高光反射部分了:

cspecular=(clightmspecular)max(0,vr)mglossc_{specular} = (c_{light} \cdot m_{specular})max(0, v \cdot r)^{m_{gloss}}

其中,mglossm_{gloss} 是材质的光泽度(gloss),也称为反光度(shininess)。它用于控制高光区域的亮点有多宽,mglossm_{gloss} 越大,亮点越小。mspecularm_{specular} 是材质的高光反射颜色,用于控制该材质高光反射颜色和强度,clightc_{light} 是光源的颜色和强度。同时这里也要防止vrv \cdot r 的值是负数。

与Phong模型相比,Blinn提出了一个简单的修改方法来得到类似的结果,其思想是,为了避免计算反射方向r,为此Blinn引入了一个新的矢量h,它是通过对v和I取平均值之后再归一化得到的,即:

h=v+Iv+Ih = \frac{v + I}{|v + I|}

然后Blinn模型的公式如下:

cspeular=(clightmspecluar)max(0,nh)mglossc_{speular} = (c_{light} \cdot m_{specluar})max(0, n \cdot h)^{m_{gloss}}

在硬件实现的时候,如果摄像机和光源的距离足够远,Blinn模型会快于Phong模型,这是因为,此时可以认为v和I都是定值,因此h会是一个常量,但是当v或者I不是定值的时候,Phong模型可能反而更快一些。

注意,Phong模型和Blinn模型都是经验模型,不应该认为一个比另一个更加正确。

逐像素还是逐顶点

上面给出了基本光照模式使用的数学公式,那么我们应该在那里进行这些计算呢?通常来讲,我们有两种选择,在片元着色器中计算,也称为逐像素光照,在顶点着色器中计算也称为逐顶点光照。

在逐像素光照中,我们会以每个像素为基础,得到它的法线(可以是对顶点法线差值得到的,也可以是从法线纹理中采样得到的),然后进行光照光照模型的计算。这种在面片之间对顶点法线进行插值的技术我们称之为Phong着色,也被称为Phong插值或法线插值技术,这不同于我们之前讲的Phong光照模型。

与之对应的是逐顶点光照,也被称为高洛德着色(Gouraud Shading),在逐顶点光照中,我们在每个定点计算光照,然后在渲染图元内部进行线性插值,最后输出成像素颜色。

由于顶点数目往往小于像素树木,因此逐顶点光照的计算量往往小于逐像素光照。但是由于逐顶点光照依赖线性插值来得到像素光照,因此当光照模型中有非线性计算(如高光反射时)时,逐顶点光照就会出现问题。

Unity中的环境光和自发光

大多数物体是没有自发光特性的。而要加上自发光属性也很简单,只需要在片元着色器最后输出的颜色之前加上发光颜色就好。

如何实现一个漫反射光照模型

现在我们正式尝试写一个漫反射的Shader,具体的原理直接看代码注释

逐顶点光照

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
Shader "Unlit/DiffuseVertexLevel"
{
Properties
{
// 声明一个属性,用于外部传入,类型为Color,默认值是白色
_Diffuse("Diffuse", Color) = (1,1,1,1)
}
SubShader
{
// 渲染类型为不透明物体
Tags { "RenderType"="Opaque" }
// LOD(Leevl of Drtail)值时100
LOD 100

Pass
{
Tags{
//定义该Pass在Unity的流水线中的角色 (只有定义它才能获取到一些Unity内置的光照变量如_LightColor0
//在默认流水线中,该光照模式是ForwardBase等
"LightMode" = "UniversalForward"
}
CGPROGRAM
#include "Lighting.cginc"

#pragma vertex vert
#pragma fragment frag

fixed4 _Diffuse;

struct a2v
{
float4 vertex: POSITION;
float3 normal: NORMAL;
};

struct v2f
{
float4 pos: SV_POSITION;
fixed3 color: COLOR;
};

v2f vert (a2v v)
{
v2f o;
// 顶点着色器必备,将顶点坐标从模型空间转化到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
// 获得环境光的值
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

// 顶点法线右乘世界空间到模型空间矩阵变换矩阵,然后归一化,获得该顶点法线在世界坐标中的向量
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));

// 当场景中只有一个光源时,_WorldSpaceLightPos0.xyz即为该光源
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);

// dot:点乘
// saturate:入参小于0,返回0,大于1时,返回1,否则返回原值
// 像素值相乘的结果是颜色混合
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));

// 像素值相加的结果是颜色叠加
o.color = ambient + diffuse;
return o;
}

fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}

ENDCG
}
}

Fallback "Diffuse"
}

逐像素光照

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
Shader "Unlit/DiffusePixelLevel"
{
Properties
{
_Diffuse("Diffuse", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
Tags{
//定义该Pass在Unity的流水线中的角色 (只有定义它才能获取到一些Unity内置的光照变量如_LightColor0
//在默认流水线中,该光照模式是ForwardBase等
"LightMode" = "UniversalForward"
}
CGPROGRAM
#include "Lighting.cginc"

#pragma vertex vert
#pragma fragment frag

fixed4 _Diffuse;

struct a2v
{
float4 vertex: POSITION;
float3 normal: NORMAL;
};

struct v2f
{
float4 pos: SV_POSITION;
float3 worldNorma: TEXCOORD0;
};

v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNorma = mul(v.normal, (float3x3)unity_WorldToObject);
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNorma);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

fixed3 color = ambient + diffuse;
return fixed4(color, 1.0);
}

ENDCG
}
}

Fallback "Diffuse"
}

如何实现一个高光反射光照模型

逐顶点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
Shader "Unlit/SpecularVertexLevel"
{
Properties
{
_Diffuse("Diffuse", Color) = (1,1,1,1)
_Specular("Specular", Color) = (1,1,1,1)
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
Tags{"LightMode" = "UniversalForward"}
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

struct a2v
{
float4 vertex: POSITION;
float3 normal: NORMAL;
};

struct v2f
{
float4 pos: SV_POSITION;
float3 color: COLOR;
};

v2f vert(a2v v)
{
v2f o;

o.pos = UnityObjectToClipPos(v.vertex);

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

fixed3 worldNormal = normalize(mul(v.normal, (fixed3x3)unity_WorldToObject));

fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));

fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);

fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);

o.color = ambient + diffuse + specular;

return o;
}

fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}
ENDCG
}
}

Fallback "Specular"
}

逐像素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
Shader "Unlit/SpecularPixelLevel"
{
Properties
{
_Diffuse("Diffuse", Color) = (1,1,1,1)
_Specular("Specular", Color) = (1,1,1,1)
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
Tags{"LightMode" = "UniversalForward"}
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

struct a2v
{
float4 vertex: POSITION;
float3 normal: NORMAL;
};

struct v2f
{
float4 pos: SV_POSITION;
float3 worldNormal: TEXCOORD0;
float3 worldPos: TEXCORRD1;
};

v2f vert(a2v v)
{
v2f o;

o.pos = UnityObjectToClipPos(v.vertex);

o.worldNormal = mul(v.vertex, (float3x3)unity_WorldToObject);

o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

fixed3 worldNormal = normalize(i.worldNormal);

fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));

fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);

fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);

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

}
ENDCG
}
}

Fallback "Specular"
}