概述
Phong模型是一种用于计算3D物体表面的光照效果的经典算法,它得名于其发明者Bui Tuong Phong(https://en.wikipedia.org/wiki/Bui_Tuong_Phong)。它主要由三个成分构成:环境光、漫反射和镜面反射。
1.环境光:表示整个场景中的均匀光照,不会受到物体表面属性的影响。
2.漫反射:表示入射光线与物体表面之间的散射,使物体表面上的每个点都能接收到来自光源的光线。其中平行光与点光源和漫反射有直接关系。
3.镜面反射:表示入射光线经过反射后集中在一个方向,形成高光的效果,模拟光线在镜面上反射的现象。
Phong模型通过将这三种光照成分相加得到最终的颜色值,并且可以根据物体表面的材质属性(如反射率、粗糙度等)调整每种光照成分的权重,从而实现更真实的光照效果。以下是phong模型的计算公式:
光照类型
法线
平行光
点光源
环境光
镜面反射/高光
Phong模型我们不再过多的解释,今天我们只讲解其中一小部分,“镜面反射”。下面是“镜面反射”的原理图。
在现实生活中,金属和抛光表面等材料具有镜面反射,根据相机角度或观察者面对物体的位置,它们看起来会更亮。因此,我们的目的是求的相机视角和反射光线的夹角,想象一下,如果我们在太阳光下看一个镜子,镜子里反射回来很亮很亮的一个光斑。这就是其中一个高光的现实例子。
基础知识
基础知识我们不过多去赘述,大家回忆一下向量,向量加法,向量的点乘以及向量的模等概念,如果不明白的可以翻翻以前的文章或者自行百度一下。
向量投影
向量投影我们需要复习下,上图表达的是向量l在向量n上的投影向量,那么用数学公式表示如下所示:
P
(
l
)
=
n
^
∗
f
P(l)=\hat{n}∗f
P(l)=n^∗f
其中 f 是一个标量,是 l 在 n 上的投影长度,向量 n ^ 是向量 n 的单位向量。 其中f是一个标量,是l在n上的投影长度,向量\hat{n}是向量n的单位向量。 其中f是一个标量,是l在n上的投影长度,向量n^是向量n的单位向量。
现在我们来求f的值:
f
=
∣
l
∣
c
o
s
(
θ
)
f = |l|cos(θ)
f=∣l∣cos(θ)
那么得:
P
(
l
)
=
n
^
∗
∣
l
∣
c
o
s
(
θ
)
P(l)=\hat{n}∗ |l|cos(θ)
P(l)=n^∗∣l∣cos(θ)
再之后我们根据点乘公式可以得到:
l
⋅
n
=
∣
l
∣
∣
n
∣
c
o
s
(
θ
)
l·n = |l||n|cos(θ)
l⋅n=∣l∣∣n∣cos(θ)
c o s ( θ ) = ( l ⋅ n ) / ∣ l ∣ ∣ n ∣ cos(θ) =(l·n)/|l||n| cos(θ)=(l⋅n)/∣l∣∣n∣
进而我们得到:
P
(
l
)
=
n
^
∗
∣
l
∣
(
(
l
⋅
n
)
/
∣
l
∣
∣
n
∣
)
=
n
^
∗
(
l
⋅
n
)
/
∣
n
∣
P(l) = \hat{n} * |l|((l·n)/|l||n|) = \hat{n} * (l·n)/|n|
P(l)=n^∗∣l∣((l⋅n)/∣l∣∣n∣)=n^∗(l⋅n)/∣n∣
再变形:
P
(
l
)
=
n
/
∣
n
∣
∗
(
l
⋅
n
)
/
∣
n
∣
=
n
∗
(
l
⋅
n
)
/
∣
n
∣
2
=
n
∗
(
l
⋅
n
)
/
(
n
⋅
n
)
P(l) = n/|n| * (l·n)/|n|=n * (l·n)/|n|²=n * (l·n)/(n·n)
P(l)=n/∣n∣∗(l⋅n)/∣n∣=n∗(l⋅n)/∣n∣2=n∗(l⋅n)/(n⋅n)
反射向量
下图是反射的向量模型,我们最终目的是要求R向量。我们已知的参数值是向量L和向量N。
我们由上面向量L在向量N的投影等于:
P
(
l
)
=
n
∗
(
l
⋅
n
)
/
(
n
⋅
n
)
P(l) = n * (l·n)/(n·n)
P(l)=n∗(l⋅n)/(n⋅n)
那么我们可以求的图上黄色细线的向量为:
Q
=
2
(
L
−
P
(
l
)
)
Q= 2(L-P(l))
Q=2(L−P(l))
那么向量R等于
L
−
R
=
Q
L-R = Q
L−R=Q
所以:
R
=
L
−
Q
=
L
−
2
(
L
−
P
(
l
)
)
=
2
P
(
l
)
−
L
=
2
∗
n
∗
(
l
⋅
n
)
/
(
n
⋅
n
)
−
l
R = L-Q = L-2(L-P(l))= 2P(l)-L =2*n * (l·n)/(n·n)-l
R=L−Q=L−2(L−P(l))=2P(l)−L=2∗n∗(l⋅n)/(n⋅n)−l
最后,如果 n 首先是一个单位向量(长度为 1 的向量),然后 ∣ n ∣ = 1 ,还有就是向量 n ^ = n 。 最后,如果n首先是一个单位向量(长度为 1 的向量),然后|n|=1,还有就是向量\hat{n}=n。 最后,如果n首先是一个单位向量(长度为1的向量),然后∣n∣=1,还有就是向量n^=n。
所以最终公式变形为:
R
=
2
∗
n
∗
(
l
⋅
n
)
−
l
R =2*n * (l·n)-l
R=2∗n∗(l⋅n)−l
应用
上述推导出来的公式实际上在glsl已经给我们封装成一个函数名字叫做:reflect
,我再glsl中级课程中讲到了此函数的应用。
求出反射光线来我们需要和相机的向量求的二者之间的夹角,就像上面我们提到,如果相机方向和反射方向正好在一条直线上,那么此时肯定是最了“刺眼”的,所以我需要又要用到点乘然后判断二者之间的夹角。代码一版如下所示:
float dotRV = clamp(dot(reflect(lightDirection, normal), -eyedirection), 0., 1.);
一般如果你觉得高亮对比度还不够高,我们可以用pow函数去加大对比度。
vec3 specular = pow(dotRV, 50.) ;
阴影
我们实现阴影效果使用的是叫阴影映射的技术, 而实现阴影映射需要用到帧缓冲区。默认情况下,WebGL 在颜色缓冲区绘图,使用隐藏面消除的话,还会用到深度缓冲区。即正常绘制的情况下包含:
- 颜色缓冲区
- 深度缓冲区
帧缓冲区对象 framebuffer object可以用来代替颜色缓冲区或深度缓冲区。绘制在帧缓冲区中的对象并不会直接显示canvas上,可以先对帧缓冲区中的内容进行一些处理再显示,或者直接用其中的内容作为纹理图像。在帧缓冲区中进行绘制的过程又称为离屏绘制 offscreen drawing。
绘制操作并不是直接发生在帧缓冲区中,而是发生在帧缓冲区所关联的对象 attachment上,一个帧缓冲区有3个关联对象:
颜色关联对象 color attachment,对应颜色缓冲区
深度关联对象 depth attachment,对应深度缓冲区
模板关联对象 stencil attachment,对应模板缓冲区。
而我们现在先有这个概念,来看看帧缓冲区的创建和配置:
- 创建帧缓冲区对象 gl.createFramebffer().
- 创建文理对象并设置其尺寸和参数gl.createTexture()、gl.bindTexture()、gl.texImage2D()、gl.Parameteri().
- 创建渲染缓冲区对象 gl.createRenderbuffer().
- 绑定渲染缓冲区对象并设置其尺寸gl.bindRenderBuffer()、gl.renderbufferStorage().
- 将帧缓冲区的颜色关联对象指定为一个文理对象gl.frambufferTexture2D().
- 将帧缓冲区的深度关联对象指定为一个渲染缓冲区对象gl.framebufferRenderbuffer().
- 检查帧缓冲区是否正确配置gl.checkFramebufferStatus().
- 在帧缓冲区中进行绘制 gl.bindFramebuffer().
阴影映射的原理很简单,首先从光的角度渲染场景,从光的角度看到的所有东西都被点亮了,而看不见的部分一定是在阴影里.。想象有一个盒子和它的光源照射下的地板,由于光源会看到这个盒子而它后面的地板部分是看不到的.那么当视线角度变化的时候,从光源角度照不到的那部分地板就渲染为阴影,原理如下图
接着我们使用阴影映射的算法实现, 它要使用到前面介绍的帧缓冲区. 阴影映射要渲染两遍:
从光源的角度渲染场景,同时把场景的深度值当成纹理渲染到帧缓冲区,也就是把它当作数据容器.
从眼睛的角度渲染场景,把物体真正渲染到画布中,同时对比纹理的深度值,将阴影部分也渲染出来.