图形学基础—传统的经验光照模型

光照模型

当光照射到物体表面时,物体会对光会发生反射、透射、吸收、衍射、折射和干涉。

其中被物体吸收的部分转化为了热,而其他形式的光进入了人的眼睛,成为了我们看到的样子。

为了模拟这些现象,人们建立了一些数学模型,用来代替现实生活中复杂的物理模型,这些数学模型就称为明暗效应模型或者光照模型。

这里只涉及一些传统的经验光照模型(Lambert模型、Half Lambert模型、Phong模型、Blinn-Phong模型)。

无光照模型

假设渲染一个物体,但是不应用任何光照模型,那么事实上,就只是把模型的贴图显示出来而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
// vertexShader
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

// fragmentShader
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
gl_FragColor = texture2D( uTexture, vUv );
}

不应用光照模型虽然也能看到物体,但是立体感较差,并且不会有光线的明暗信息

定义一些方向

使用光照模型之前首先需要定义一些东西。

我们考虑的着色结果,都是考虑在某一个点上应该是什么样的结果,而这个点通常称为着色点(shading point)。

每一个shading point都应该在物体表面上。虽然,物体表面可能是一个曲面,但是仍然可以认为,它是由很多小的平面构成的。

在一个平面内,可以定义:

  1. 平面的法线方向n:垂直平面,并由内指向外。
  2. 观测方向v:shading point与相机位置的连线方向。
  3. 光源方向l:shading point与光源位置的连线方向。

定义参数

Lambert模型

Lambert模型是一种经验模型,主要用来简单的模拟粗糙物体表面的光照现象。

从日常生活中可以发现,一个物体面向光源的地方更亮,而背向光源的地方更暗

而在一个shading point中判断其是否面向光源,其实可以通过判断该shading point的法向量n是否与光源方向l足够接近,也就是他们之间的夹角是否足够小。夹角越小,光线越强,夹角越大,光线越弱。

图形学基础—向量 _ 向量的点乘在图形学上的作用中有提到,通过向量的点乘就可以判断两个向量之间是否足够接近。

Lambert模型分为两个部分:环境光、漫反射。

ILambert=ambient+diffuseILambert = ambient + diffuse

环境光

环境光和其他光照模型一样

ambient=KaambientColorambient = K_a * ambientColor

  • KaK_a:材质的环境光反射系数
  • ambientColorambientColor:是环境光强度和颜色数值,通常是vec3类型

漫反射

漫反射项也和其他模型一样

diffuse=KllightColormax(0.0,dot(N,L))diffuse = K_l * lightColor * max(0.0, dot(N, L))

  • KlK_l:材质的灯光反射系数(如果使用贴图,那就用贴图颜色作为漫反射系数)
  • lightColorlightColor:是灯光强度和颜色数值,通常是vec3类型
  • NN:法向量方向
  • LL:光线方向

最终颜色

最终Lambert模型的颜色为:

finalColor=ambient+diffusefinalColor = ambient + diffuse

  • ambientambient:环境光
  • diffusediffuse: 漫反射

glsl

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
// vertexShader
uniform vec4 lightPosition;
varying vec2 vUv;
varying vec3 vNormal;
varying vec4 vPosition;
varying vec4 vlightPosition;

void main() {
vUv = uv;
vNormal = normalize( normalMatrix * normal );
vPosition = modelViewMatrix * vec4(position, 1.0);
vlightPosition = modelViewMatrix * lightPosition;
gl_Position = projectionMatrix * vPosition;
}

// fragmentShader
uniform sampler2D uTexture;
uniform vec3 ambientColor; // 环境光颜色
uniform vec3 lightColor; // 灯光颜色
uniform float K_a; // 环境光反射系数

varying vec2 vUv;
varying vec3 vNormal;
varying vec4 vPosition;
varying vec4 vlightPosition;

void main() {
vec3 vL = normalize(vlightPosition.xyz - vPosition.xyz);
vec3 ambient = ambientColor * K_a;
vec3 K_l = texture2D( uTexture, vUv ).rgb;
vec3 diffuse = K_l * lightColor * max(0.0, dot(vNormal, vL));
gl_FragColor = vec4(ambient + diffuse,1.0);
}

着色效果

可以看到加了Lambert模型之后,渲染效果比之前好多了,增加了立体感。

但是还有一个弊端:在光源照不到的地方,Lambert模型表现的效果是全黑的。但事实上,我们日常所处的环境中很多时候会有环境光,不会导致全黑的效果。

而另一个简单的经验模型就弥补了这一弊端。

Half Lambert模型

由于向量的点乘结果在-1到1之间。而在光照模型中,我们只会使用0到1之间的值,小于0的值认为该光源被遮挡对着色没有影响。

也正是因为我们使用了0到1之间的值,所以导致在光源无法照射到的地方,着色结果是全黑的。

而Half Lambert模型想法很简单,就是把0到1之间的值变成0.5到1

有趣的是Half Lambert光照模型是Valve公司在制作“半条命(Half-Life)”的时候发明的。而这个模型的想法正好也与Half有关。

HalfLambert模型

Half Lambert模型的漫反射

Half Lambert模型与Lambert模型的区别在漫反射公式,Half Lambert模型的漫反射公式:

diffuse=KllightColor(max(0.0,dot(N,L))0.5+0.5)diffuse = K_l * lightColor * (max(0.0, dot(N, L)) * 0.5 + 0.5)

  • KlK_l:材质的灯光反射系数(如果使用贴图,那就用贴图颜色作为漫反射系数)
  • lightColorlightColor:是灯光强度和颜色数值,通常是vec3类型
  • NN:法向量方向
  • LL:光线方向

glsl

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
// vertexShader
uniform vec4 lightPosition;
varying vec2 vUv;
varying vec3 vNormal;
varying vec4 vPosition;
varying vec4 vlightPosition;

void main() {
vUv = uv;
vNormal = normalize( normalMatrix * normal );
vPosition = modelViewMatrix * vec4(position, 1.0);
vlightPosition = modelViewMatrix * lightPosition;
gl_Position = projectionMatrix * vPosition;
}

// fragmentShader
uniform sampler2D uTexture;
uniform vec3 ambientColor; // 环境光颜色
uniform vec3 lightColor; // 灯光颜色
uniform float K_a; // 环境光反射系数

varying vec2 vUv;
varying vec3 vNormal;
varying vec4 vPosition;
varying vec4 vlightPosition;

void main() {
vec3 vL = normalize(vlightPosition.xyz - vPosition.xyz);
vec3 ambient = ambientColor * K_a;
vec3 K_l = texture2D( uTexture, vUv ).rgb;
vec3 diffuse = K_l * lightColor * (max(0.0, dot(vNormal, vL)) * 0.5 + 0.5);
gl_FragColor = vec4(ambient + diffuse,1.0);
}

着色效果

可以看到使用了Half Lambert模型之后,渲染效果相比于使用Lambert模型,在暗部变得更亮了,更符合现实场景。

Phong模型

在日常生活中,我们可以发现,在一些光滑的物体表面,除了会有类似Lambert模型那样的明暗关系,还会有一些“布灵布灵”的高光。

并且这些高光的位置还会随着观察的方向变化而变化。

而Phong模型就是在Lambert模型下增加了高光项。

因此Lambert模型通常用来描述粗糙的物体表面,而Phong模型通常用来描述光滑的物体表面

Phong模型分为三个部分:环境光、漫反射、高光。

Iphong=ambient+diffuse+specColorIphong = ambient + diffuse + specColor

环境光

环境光和其他光照模型一样

ambient=KaambientColorambient = K_a * ambientColor

  • KaK_a:材质的环境光反射系数
  • ambientColorambientColor:是环境光强度和颜色数值,通常是vec3类型

漫反射

漫反射项也和其他模型一样(这里为了使得暗部显示正常,所以用了Half Lambert模型)

diffuse=KllightColor(max(0.0,dot(N,L))0.5+0.5)diffuse = K_l * lightColor * (max(0.0, dot(N, L)) * 0.5 + 0.5)

  • KlK_l:材质的灯光反射系数(如果使用贴图,那就用贴图颜色作为漫反射系数)
  • lightColorlightColor:是灯光强度和颜色数值,通常是vec3类型
  • NN:法向量方向
  • LL:光线方向

高光

要计算高光,首先需要知道高光产生的原因。

高光产生的原因

高光是由于物体表面较为光滑,对光的反射较强。

当光线照射到物体表面之后会被反射出来。

如果此时我们看向的方向正好与反射的光线方向很接近,就会看到一个很明显的光斑,这个光斑就是我们所说的高光。

产生高光的原因

如图所示,光线沿着-l方向照射到物体表面,并且沿着R方向被反射出来,如果此时R方向与V方向足够接近,那么就会看到一个高光。

高光的计算

高光是由于反射光的方向与观测方向足够接近,那么我们就需要先计算反射光方向。

推导反射方向

如上图所示,L\vec{L}为光线方向,R\vec{R}为反射方向,n\vec{n}为法线方向。

由于向量平移之后仍然可以表示本身,因此可以将R\vec{R}平移,使得R\vec{R}的起点与L\vec{L}的终点重合。

假设B=L+R\vec{B}=\vec{L} + \vec{R},则向量B\vec{B}一定在n\vec{n}方向上。

并且假设L\vec{L}n\vec{n}方向上的投影为b\vec{b},那么b=(Ln)n\vec{b} = (\vec{L} \cdot \vec{n}) * \vec{n}

由于L\vec{L}R\vec{R}的长度相同,因此向量L\vec{L}R\vec{R}B\vec{B}组成的三角形是等腰三角形。

从而B=2b\vec{B}=2 * \vec{b},也就是2b=L+R=2(Ln)n2 * \vec{b} =\vec{L} + \vec{R} = 2 * (\vec{L} \cdot \vec{n}) * \vec{n}

所以反射方向R=2(Ln)nL\vec{R} = 2 * (\vec{L} \cdot \vec{n}) * \vec{n} - \vec{L}

知道反射方向之后,就可以写出高光的表达式:

specColor=KshighlightColormax(0.0,dot(v,R))specColor = K_s * highlightColor * max(0.0, dot(v, R))

  • KsK_s:材质的高光反射系数
  • highlightColorhighlightColor:是高光强度和颜色数值,通常是vec3类型
  • vv:观测方向
  • RR:光线反射方向

渲染结果如下:

phong
现实中的高光

从渲染结果中可以看到,确实相比于Half Lambert模型增加了一些高光项,但是高光范围很大,不太像我们在现实生活中看见的小的光斑。

因此我们需要用指数函数缩小光斑的范围,修改之后的高光表达式为:

specColor=KshighlightColorpow(max(0.0,dot(v,R)),smoothness)specColor = K_s * highlightColor * pow(max(0.0, dot(v, R)), smoothness)

  • smoothnesssmoothness:高光的平滑度

设置平滑度smoothness为40之后,可以看到光斑的范围缩小了很多,更符合现实生活中看到的样子。

phong用指数函数调整高光

glsl

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
// vertexShader
uniform vec4 lightPosition;
uniform vec4 cPosition;
varying vec2 vUv;
varying vec3 vNormal;
varying vec4 vPosition;
varying vec4 vlightPosition;
varying vec4 vCPosition;

void main() {
vUv = uv;
vNormal = normalize( normalMatrix * normal );
vPosition = modelViewMatrix * vec4(position, 1.0);
vlightPosition = modelViewMatrix * lightPosition;
vCPosition = modelViewMatrix * cPosition;
gl_Position = projectionMatrix * vPosition;
}

// fragmentShader
uniform sampler2D uTexture;
uniform vec3 ambientColor; // 环境光颜色
uniform vec3 lightColor; // 灯光颜色
uniform vec3 highlightColor; // 高光颜色
uniform float K_a; // 环境光反射系数
uniform float K_s; // 高光反射系数
uniform float smoothness; // 高光平滑度

varying vec2 vUv;
varying vec3 vNormal;
varying vec4 vPosition;
varying vec4 vlightPosition;
varying vec4 vCPosition;

void main() {
vec3 ambient = ambientColor * K_a;
vec3 vL = normalize(vlightPosition.xyz - vPosition.xyz);
vec3 K_l = texture2D( uTexture, vUv ).rgb;
vec3 diffuse = K_l * lightColor * (max(0.0, dot(vNormal, vL)) * 0.5 + 0.5);
vec3 v = normalize(vCPosition.xyz - vPosition.xyz);
vec3 R = 2.0 * dot(vNormal, vL) * vNormal - vL;
// 反射光线也可以使用reflect函数计算,只不过与我们推倒的公式不同
// reflect的入射光方向是从光源指向平面的,而我们推导的公式,是从平面指向光源的。
vec3 specColor = K_s * highlightColor * pow(max(0.0, dot(v, R)), smoothness);
gl_FragColor = vec4(ambient + diffuse + specColor, 1.0);
}

Phong模型着色效果

Blinn-Phong模型

前面有说到高光是因为反射的光线方向与观测方向接近,因此计算高光之前我们需要计算反射的方向。

反射的方向计算比较复杂,比较耗时,而Blinn-Phong模型则解决了这一问题。

高光的计算

人们巧妙地发现了其实只需要计算光线方向l\vec{l} 与观测方向v\vec{v} 的半程向量h\vec{h} 是否与法向量n\vec{n} 足够接近就可以了

Blinn-Phong模型

而半程向量的计算非常简单,只需要将两个向量相加,再单位化即可。

h=normalize(l+v)\vec{h} = normalize(\vec{l} + \vec{v})

因此Blinn-Phong光照模型的高光项就可以表示为:

specColor=KshighlightColorpow(max(0.0,dot(n,h)),smoothness)specColor = K_s * highlightColor * pow(max(0.0, dot(n, h)), smoothness)

  • nn:法向量
  • hh:半程向量

Blinn-Phong光照模型极大的简化了Phong模型中的高光计算,并且能够达到与Phong模型类似的效果。

glsl

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
// vertexShader
uniform vec4 lightPosition;
uniform vec4 cPosition;
varying vec2 vUv;
varying vec3 vNormal;
varying vec4 vPosition;
varying vec4 vlightPosition;
varying vec4 vCPosition;

void main() {
vUv = uv;
vNormal = normalize( normalMatrix * normal );
vPosition = modelViewMatrix * vec4(position, 1.0);
vlightPosition = modelViewMatrix * lightPosition;
vCPosition = modelViewMatrix * cPosition;
gl_Position = projectionMatrix * vPosition;
}

// fragmentShader
uniform sampler2D uTexture;
uniform vec3 ambientColor; // 环境光颜色
uniform vec3 lightColor; // 灯光颜色
uniform vec3 highlightColor; // 高光颜色
uniform float K_a; // 环境光反射系数
uniform float K_s; // 高光反射系数
uniform float smoothness; // 高光平滑度

varying vec2 vUv;
varying vec3 vNormal;
varying vec4 vPosition;
varying vec4 vlightPosition;
varying vec4 vCPosition;

void main() {
vec3 ambient = ambientColor * K_a;
vec3 vL = normalize(vlightPosition.xyz - vPosition.xyz);
vec3 K_l = texture2D( uTexture, vUv ).rgb;
vec3 diffuse = K_l * lightColor * (max(0.0, dot(vNormal, vL)) * 0.5 + 0.5);
vec3 v = normalize(vCPosition.xyz - vPosition.xyz);
vec3 h = normalize(v + vL);
vec3 specColor = K_s * highlightColor * pow(max(0.0, dot(vNormal, h)), smoothness);
gl_FragColor = vec4(ambient + diffuse + specColor, 1.0);
}

Blinn-Phong模型着色效果


图形学基础—传统的经验光照模型
https://www.liaomz.top/2022/03/12/tu-xing-xue-ji-chu-chuan-tong-de-jing-yan-guang-zhao-mo-xing/
作者
发布于
2022年3月12日
许可协议