经典光照模型

概述

当光照射到物体表面时,一部分被物体表面吸收,另一部分被反射,对于透明物体而言,还有一部分光穿过透明体,产生透射光。被物体吸收的光能转化为热量,只有反射光和透射光能够进入眼睛,产生视觉效果。通过反射和透射产生的光波(光具有波粒二相性)决定了物体呈现的亮度和颜色,即反射和投射光的强度决定了物体表面的亮度,而它们含有的不同波长光的比例决定了物体表面的色彩。

所以,物体表面光照颜色由入射光、物体材质,以及材质和光的交互规律共同决定。

光与物体最基本的交互方式就是反射,遵循反射定律:反射光与入射光位于表面法向两侧,对理想反射面(如镜面),入射角等于反射角,观察者只能在表面法向的反射方向一侧才能看到反射光。

光源

环境光(Ambient Light):从物体表面所产生的反射光的统一照明,称为环境光或背景光(计算机图形学第二版 389 页)。例如房间里面并没有受到灯光或者太阳光的直接照射,而是由墙壁、天花板、地板及室内各物体之间光的多次反射进行自然照明。通常我们认为理想的环境光具有如下特性:没有空间或方向性;在所有方向上和所有物体表面上投射的环境光强度是统一的恒定值。

由于环境光给予物体各个点的光照强度相同,且没有方向之分,所以在只有环境光的情况下,同一物体各点的明暗程度均一样,因此,只有环境光是不能产生具有真实感的图形效果。

环境光是对光照现象的最简单抽象,局限性很大。它仅能描述光线在空间中无方向并均匀散布时的状态。真实的情况是:光线通常都有方向。点光源是发光体的最简单的模型,光线由光源出发向四周发散。还有一种是平行光,即光线都从同一个方向照射。通过模拟方向光和物体表面的交互模式,可以渲染出具有高真实感(明暗变化、镜面反射等)的三维场景。

漫反射与 Lambert 模型

粗糙的物体表面向各个方向等强度地反射光,这种等同地向各个方向散射的现象称为光的漫反射(diffuse reflection)。产生光的漫反射现象的物体表面称为理想漫反射体,也称为朗伯(Lambert)反射体。

Lambert 实现出来的效果,一旦入射光向量与材质表面的角度大于90度,那么得到的漫反射颜色就会全部变为黑色,没有任何明暗变化效果。

Lambert 光照模型公式:
最终颜色 = 直射光颜色 * 漫反射颜色 * max(0, dot(光源方向, 法线方向))

其中,直射光颜色,漫反射颜色,都是我们自定义的变量。

Half Lambert 模型

Half Lambert 是在 Lambert 模型的基础上,做了微调,也就是将光源方向与法线方向的点乘结果,从原来[-1, 1],映射为 [0, 1],这样原来背光面,也会有明暗效果。

Half Lambert 光照模型公式:
最终颜色 = 直射光颜色 * 漫反射颜色 * (dot(光源方向, 法线方向) * 0.5 + 0.5)

镜面反射与 Phong 模型

Lambert 模型较好地表现了粗糙表面上的光照现象,如石灰粉刷的墙壁、纸张等,但在用于诸如金属材质制成的物体时,则会显得呆板,表现不出光泽,主要原因是该模型没有考虑这些表面的镜面反射效果。一个光滑物体被光照射时,可以在某个方向上看到很强的反射光,这是因为在接近镜面反射角的一个区域内,反射了入射光的全部或绝大部分光强,该现象称为镜面反射。

Phone 模型,的原理很简单,想象一束光射向某个点,然后反射出去,我们的眼睛同样看向那个点,当我们的眼睛看向那个点的方向,与光线反射的方向,越接近时,进入我们眼睛的反射光则越多,也就是更亮。看下面的图:

很明显,当视野方向与光的反射方向夹角越小时,也就是说进入眼睛的光越多,所以那个点也就会越亮,这就是高光反射的原理。所以高光反射,实现起来也就很简单了,只要拿到视野方向,拿到直射光的反射方向,就可以求出最终的颜色值。

Phong 光照模型公式:
最终颜色 = 直射光颜色 * 反射光颜色 * pow(max(0, dot(反射光方向, 视野方向)), 光泽度(gloss)) + 漫反射颜色 + 环境光颜色

其中,光泽度用于控制高光区域的亮点大小,gloss 值越大,亮点越小。

Blinn-Phong 光照模型

Blinn-Phong 光照模型,又称为 Blinn-phong 反射模型(Blinn–Phong reflectionmodel)或者 phong 修正模型(modified Phong reflection model),是由 Jim Blinn于 1977 年在文章“Models of light reflection for computer synthesized pictures”中对传统 phong 光照模型基础上进行修改提出的。和传统 phong 光照模型相比,Blinn-phong 光照模型混合了 Lambert 的漫射部分和标准的高光,渲染效果有时比 Phong 高光更柔和、更平滑,此外它在速度上相当快,因此成为许多 CG 软件中的默认光照渲染方法。此外它也集成在了大多数图形芯片中,用以产生实时快速的渲染。在 OpenGL 和 Direct3D 渲染管线中,Blinn-Phong 就是默认的渲染模型。

Phone 模型有一些缺点,所以后来出现了改进的模型,Blinn-Phone,对 Phone 模型进行了微调。Phone 模型的高光强度,是由光线的反射方向与视野方向的夹角决定的。而 Blinn-Phone 的模型,只是把反射方向和视野方向换成,法线方向,和视野与光线方向的中间向量之间的夹角。看下面的图

Blinn-Phone 高光反射公式:
最终颜色 = 直射光颜色 * 反射光颜色 * pow(max(0, dot(法线方向, 视野与光线中间向量)), 光泽度(gloss)) + 漫反射颜色 + 环境光颜色

关于 Phone 和 Blinn-Phone 更详细的一篇文章,learnopengl-cn 上的,推荐大家阅读 点这里

实现上面的四种光照模型

接下来的实现,都是在片元函数中实现。当然,也可以放到顶点函数中实现,只是放到片元函数中效果会更平滑一些,但是相比放在顶点函数中做计算,耗费的性能也会更多一点。

先看一下四种实现的效果对比

从正面看

从背面看

从下面看

Lambert:

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
Shader "iMoeGirl/Lambert" {

Properties {
_Diffuse("Diffuse Color", Color) = (1,1,1,1)
}

SubShader{
Pass{
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag

fixed4 _Diffuse;

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

struct v2f {
float4 svPos: SV_POSITION; // 这个是必须的,否则显示不出来
fixed3 normalizedWorldNormal : COLOR;
};

v2f vert(a2v v) {
v2f f;

// 将模型空间的顶点坐标转换到裁剪空间
f.svPos = UnityObjectToClipPos(v.vertex);

// 将模型空间的法线转换到世界空间,然后标准化,
//(转换到世界空间是为了后面和灯光做计算)
f.normalizedWorldNormal = normalize(UnityObjectToWorldNormal(v.normal));

return f;
}

fixed4 frag(v2f f) : SV_TARGET {

// 取得灯光方向,然后标准化
float3 normalizedLightDir = normalize(_WorldSpaceLightPos0.xyz);

fixed dotValue = max(0, dot(normalizedLightDir, f.normalizedWorldNormal));

fixed3 diffuse = _LightColor0.rgb * _Diffuse * dotValue;

return fixed4(diffuse, 1);
}

ENDCG
}
}

Fallback "VertexLit"
}

Half Lambert:

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
Shader "iMoeGirl/Half-Lambert" {
Properties {
_Diffuse("Diffuse Color", Color) = (1,1,1,1)
}

SubShader{
Pass{
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag

fixed4 _Diffuse;

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

struct v2f {
float4 svPos: SV_POSITION; // 这个是必须的,否则显示不出来
fixed3 normalizedWorldNormal : COLOR;
};

v2f vert(a2v v) {
v2f f;

// 将模型空间的顶点坐标转换到裁剪空间
f.svPos = UnityObjectToClipPos(v.vertex);

// 将模型空间的法线转换到世界空间,然后标准化,
//(转换到世界空间是为了后面和灯光做计算)
f.normalizedWorldNormal = normalize(UnityObjectToWorldNormal(v.normal));

return f;
}

fixed4 frag(v2f f) : SV_TARGET {

// 取得灯光方向,然后标准化
float3 normalizedLightDir = normalize(_WorldSpaceLightPos0.xyz);

fixed dotValue = dot(normalizedLightDir, f.normalizedWorldNormal) * 0.5 + 0.5;

fixed3 diffuse = _LightColor0.rgb * _Diffuse * dotValue;

return fixed4(diffuse, 1);
}

ENDCG
}
}

Fallback "VertexLit"
}

Phong:

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
Shader "iMoeGirl/Phone" {
Properties {
_Diffuse("Diffuse Color", Color) = (1,1,1,1)
_Specular("Specular Color", Color) = (1,1,1,1)
_Gloss("Gloss", Range(10, 200)) = 20
}

SubShader{
Pass{
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

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

struct v2f {
float4 svPos: SV_POSITION; // 这个是必须的,否则显示不出来
fixed3 normalizedWorldNormal : COLOR;
float3 worldPos: TEXCOORD0; // 顶点世界坐标
};

v2f vert(a2v v) {
v2f f;

// 将模型空间的顶点坐标转换到裁剪空间
f.svPos = UnityObjectToClipPos(v.vertex);

// 将模型空间的法线转换到世界空间,然后标准化,
//(转换到世界空间是为了后面和灯光做计算)
f.normalizedWorldNormal = normalize(UnityObjectToWorldNormal(v.normal));

return f;
}

fixed4 frag(v2f f) : SV_TARGET {
// 下面先计算漫反射
// 取得灯光方向,然后标准化
float3 normalizedLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed dotValue = dot(normalizedLightDir, f.normalizedWorldNormal) * 0.5 + 0.5;
fixed3 diffuse = _LightColor0.rgb * _Diffuse * dotValue;

// 再计算高光反射
// 取得反射光方向
fixed3 reflectDir = normalize(reflect(-normalizedLightDir, f.normalizedWorldNormal));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - f.worldPos);

float specularValue = pow(max(dot(reflectDir, viewDir), 0), _Gloss);
fixed3 specular = _LightColor0.rgb * _Specular * specularValue;

// 取得环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;

// 最终颜色
fixed3 color = specular + diffuse + ambient;

return fixed4(color, 1);
}

ENDCG
}
}

Fallback "VertexLit"
}

Blinn-Phone:

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
Shader "iMoeGirl/Blinn-Phone" {
Properties {
_Diffuse("Diffuse Color", Color) = (1,1,1,1)
_Specular("Specular Color", Color) = (1,1,1,1)
_Gloss("Gloss", Range(10, 200)) = 20
}

SubShader{
Pass{
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

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

struct v2f {
float4 svPos: SV_POSITION; // 这个是必须的,否则显示不出来
fixed3 normalizedWorldNormal : COLOR;
float3 worldPos: TEXCOORD0; // 顶点世界坐标
};

v2f vert(a2v v) {
v2f f;

// 将模型空间的顶点坐标转换到裁剪空间
f.svPos = UnityObjectToClipPos(v.vertex);

// 将模型空间的法线转换到世界空间,然后标准化,
//(转换到世界空间是为了后面和灯光做计算)
f.normalizedWorldNormal = normalize(UnityObjectToWorldNormal(v.normal));

return f;
}

fixed4 frag(v2f f) : SV_TARGET {
// 下面先计算漫反射
// 取得灯光方向,然后标准化
float3 normalizedLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed dotValue = dot(normalizedLightDir, f.normalizedWorldNormal) * 0.5 + 0.5;
fixed3 diffuse = _LightColor0.rgb * _Diffuse * dotValue;

// 再计算高光反射
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - f.worldPos);
fixed3 halfDir = normalize(viewDir + normalizedLightDir);

float specularValue = pow(max(dot(f.normalizedWorldNormal, halfDir), 0), _Gloss);
fixed3 specular = _LightColor0.rgb * _Specular * specularValue;

// 取得环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;

// 最终颜色
fixed3 color = specular + diffuse + ambient;

return fixed4(color, 1);

}

ENDCG
}
}

Fallback "VertexLit"
}

本文参考:
康玉之——《GPU 编程与 CG 语言之阳春白雪下里巴人》 第 9 章 经典光照模型(illumination model)
https://imoegirl.com/2020/03/19/unity-shader-basis-05/