目录
光照模型
最常见的光照模型称为“ADS”模型:
- 环境光反射(Ambient reflection)——模拟低级光照,影响场景中的所有物体。
- 漫反射(Diffuse reflection)——根据光线的入射角度调整物体亮度。
- 镜面反射(Specular reflection)——用以展示物体的光泽,通过在物体表面上,光线最直接反射到我们的眼睛的位置,策略性地放置适当大小的高光来实现。
场景的绘制最终由片段着色器为屏幕上的每个像素输出颜色而实现。使用ADS光照模型需要指定由于像素的RGBA输出值上的光照而产生的分量。因素包括: - 光源类型及其环境、漫反射和镜面反射特性;
- 对象材质的环境、漫反射和镜面反射特性;
- 对象的材质指定为“光泽”;
- 光线照射物体的角度;
- 从中查看场景的角度。
光源
光源有许多类型,每种光源具有不同的特性。常见光源类型有:
- 全局光(通常称为“全局环境光”,因为它仅包含环境光组件);
- 定向光(或“远距离光”);
- 位置光(或“点光源”);
- 聚光灯。
➊全局环境光
是最简单的光源模型。它没有光源位置——无论场景中的对象在何处,其上的每个像素都有着相同的光照。全球环境光照模拟了现实世界中的一种光线现象,即光线经过很多次反射,其光源和方向都已经无法确定。全局环境光仅具有环境光反射分量,用RGBA 值设定;它没有漫反射或镜面反射分量。
RGBA 的取值范围为 0~1,全局环境光通常被建模为偏暗的白光,其中 RGB 各值设为0~1 的相同的小数, alpha 设置为 1。
例如,全局环境光可以定义如下:
float globalAmbient[4] = {0.6f, 0.6f, 0.6f, 1.0f};
➋定向光
或远距离光也没有源位置,但它具有方向。它可以用来模拟光源距离非常远,以至于光线接近平行的情况,例如阳光。通常在这种情况下,我们可能只对建模光照感兴趣,而对发光的物体不感兴趣。定向光对物体的影响取决于光照角度,物体在朝向定向光的一侧比在切向或相对侧更亮。建模定向光需要指定其方向(以向量形式)及其环境、漫反射和镜面特征(以 RGBA 值)。
在已经有全局环境光的情况下,定向光的环境光分量看起来似乎是多余的。然而,当光源“开启”或“关闭”时,全局环境光和定向光的环境光分量的区别就很明显了。当“开启”时,总环境光分量将如预期的那样增加。下面的示例代码中,我们只使用了很小的环境光分量。在实际场景中,应当根据场景的需要平衡两个环境光分量。
指向 Z 轴负方向的红色定向光可以指定如下:
float dirLightAmbient[4] = {0.1f, 0.0f, 0.0f, 1.0f};
float dirLightDiffuse[4] = {1.0f, 0.0f, 0.0f, 1.0f};
float dirLightSpecular[4] = {1.0f, 0.0f, 0.0f, 1.0f};
float dirLightDirection[3] = {0.0f, 0.0f, -1.0f};
➌位置光
在 3D 场景中具有特定位置。靠近场景的光源,例如台灯,蜡烛等。像定向光一样,位置光的效果取决于撞击角度;但是,它没有方向,因为它对场景中的每个顶点的光照方向都不同。位置光还可以包含衰减因子
,以模拟它们的强度随距离减小的程度。与我们看到的其他类型的光源一样,位置光具有指定为 RGBA 值的环境光反射、漫反射和镜面反射特性。
位置 (5, 2, −3) 处的红色位置光可以指定如下例:
float posLightAmbient[4] = {0.1f, 0.0f, 0.0f, 1.0f};
float posLightDiffuse[4] = {1.0f, 0.0f, 0.0f, 1.0f};
float posLightSpecular[4] = {1.0f, 0.0f, 0.0f, 1.0f};
float posLightLocation[3] = {5.0f, 2.0f, -3.0f};
衰减因子有多种建模方式。其中一种方式是使用恒定、线性和二次方(分别称为kc、kl和kq)衰减,并引入非负可调参数。
这些参数与离光源的距离(d)结合进行计算:
a
t
t
e
n
u
a
t
i
o
n
F
a
c
t
o
r
=
1
k
c
+
k
l
d
+
K
q
d
2
attenuationFactor = \frac { 1 } { k_c+k_ld+K_qd^2 }
attenuationFactor=kc+kld+Kqd21
将这个因子与光的强度相乘可以使距光更远时,光的强度衰减更多。注意, kc 应当永远设置为大于等于 1 的值,从而使得衰减因子落入[0…1]区间,并当 d 增大时接近于 0。
➍聚光灯(spotlight)
同时具有位置和方向。其“锥形”效果可以使用 0° ~90° 的截光角 θ来模拟,指定光束的半宽度,并使用衰减指数来模拟随光束角度的强度变化。如下图所示,我们确定聚光灯方向与从聚光灯到像素的向量之间的角度 φ。当 φ 小于 θ 时,我们通过将 φ的余弦提高到衰减指数来计算强度因子(当 φ 大于 θ 时,强度因子设置为 0)。结果是强度因子的范围为 0~1。衰减指数会影响当角度 φ 增加时,强度因子趋于 0 的速率。然后将强度因子乘以光的强度以模拟锥形效果。
位于( 5,2,−3)向下照射 Z 轴负方向的红色聚光灯可以表示为:
float spotLightAmbient[4] = { 0.1f, 0.0f, 0.0f, 1.0f };
float spotLightDiffuse[4] = { 1.0f, 0.0f, 0.0f, 1.0f };
float spotLightSpecular[4] = {1.0f, 0.0f, 0.0f, 1.0f };
float spotLightLocation[3] = { 5.0f, 2.0f, -3.0f };
float spotLightDirection[3] = { 0.0f, 0.0f, -1.0f };
float spotLightCutoff = 20.0f;
float spotLightExponent = 10.0f;
聚光灯也可以引入衰减因子。聚光灯衰减因子可以用与前述定向光源相同的方式实现。
当设计拥有许多光源的系统时,程序员应该考虑创建相应的类结构,如定义 Light 类以及其子类 GlobalAmbient、 Directional、 Positional 以及 Spotlight。由于聚光灯同时具有定向光和位置光的特性,这里就值得使用 C++的多继承能力,让 Spotlight 类同时继承于实现位置光和定向光的类。
材质
光照使得我们可以加入表面的反射特性。即对象如何与我们的 ADS 光照模型相互作用。这可以通过将每个对象视为“由某种材质制成”来建模。
一些其他材质的ADS RGBA值见下图:
对于大多数材质而言, 只需要把不透明度设置为 1.0就行了,但是对于某些特定的材质,加入一些透明度是很重要的。例如,上图中材质“玉”和“珍珠”都含有少量透明度(取值略微小于 1.0)以显得更加真实。
放射性
有时也包含在 ADS 材质规范中。在模拟自身发光的材质(例如磷光材质)时非常有用。
预定义一些可供选择的材质, 在使用时会很方便。 因此我们需要在 Utils.cpp 文件中添加如下代码:
// 黄金材质
float* Utils::goldAmbient() {// 环境光RGBA
static float a[4] = { 0.2473f, 0.1995f, 0.0745f, 1 };
return (float*)a;
}
float* Utils::goldDiffuse() {// 漫反射RGBA
static float a[4] = { 0.7516f, 0.6065f, 0.2265f, 1 };
return (float*)a;
}
float* Utils::goldSpecular() {// 镜面反射RGBA
static float a[4] = { 0.6283f, 0.5559f, 0.3661f, 1 };
return (float*)a;
}
float Utils::goldShininess() { return 51.2f; }// 光泽
这样在 init()函数中或全局中为物体指定“黄金”材质就非常容易了,如下所示:
float* matAmbient = Utils::goldAmbient();
float* matDiffuse = Utils::goldDiffuse();
float* matSpecular = Utils::goldSpecular();
float matShininess = Utils::goldShininess();
ADS光照计算
当我们绘制场景时,每个顶点坐标都会进行变换以将 3D 世界模拟到 2D 屏幕上。每个像素的颜色都是光栅化、纹理贴图以及插值的结果。现在我们需要加入一个新的步骤来调整这些光栅化之后的像素颜色,以便反应场景中的光照和材质。
我们需要做的基础 ADS 计算是确定每个像素的光强度:
⓿反射强度(Reflection Intensity, I)
。Iobserved计算过程如下:
I
o
b
s
e
r
v
e
d
=
I
a
m
b
i
e
n
t
+
I
d
i
f
f
u
s
e
+
I
s
p
e
c
u
l
a
r
I_{observed} = I_{ambient} + I_{diffuse}+I_{specular}
Iobserved=Iambient+Idiffuse+Ispecular
我们需要计算每个光源对于每个像素的环境光反射、漫反射和镜面反射分量,并求和。这些计算都基于场景内的光源类型以及渲染中模型的材质类型。
❶环境光分量
Iambient的值是场景环境光与材质环境光分量的乘积:
I
a
m
b
i
e
n
t
=
L
i
g
h
t
a
m
b
i
e
n
t
∗
M
a
t
e
r
i
a
l
a
m
b
i
e
n
t
I_{ambient}=Light_{ambient}*Material_{ambient}
Iambient=Lightambient∗Materialambient
光与材质亮度都是 RGB 值,计算可以更准确地描述为:
I
a
m
b
i
e
n
t
r
e
d
=
L
i
g
h
t
a
m
b
i
e
n
t
r
e
d
∗
M
a
t
e
r
i
a
l
a
m
b
i
e
n
t
r
e
d
▫
I
a
m
b
i
e
n
t
g
r
e
e
n
=
L
i
g
h
t
a
m
b
i
e
n
t
g
r
e
e
n
∗
M
a
t
e
r
i
a
l
a
m
b
i
e
n
t
g
r
e
e
n
▫
I
a
m
b
i
e
n
t
b
l
u
e
=
L
i
g
h
t
a
m
b
i
e
n
t
b
l
u
e
∗
M
a
t
e
r
i
a
l
a
m
b
i
e
n
t
b
l
u
e
I _ { ambient } ^ { red } = Light_{ambient}^{red}*Material_{ambient}^{red} \\▫ \\ I _ { ambient } ^ { green } = Light_{ambient}^{green}*Material_{ambient}^{green} \\▫ \\ I _ { ambient } ^ { blue} = Light_{ambient}^{blue}*Material_{ambient}^{blue}
Iambientred=Lightambientred∗Materialambientred▫Iambientgreen=Lightambientgreen∗Materialambientgreen▫Iambientblue=Lightambientblue∗Materialambientblue
❷漫反射分量
Idiffuse,朗伯余弦定律:表面反射的光量与光入射角的余弦成正比。(光量即光强度)
I
d
i
f
f
u
s
e
=
L
i
g
h
t
d
i
f
f
u
s
e
∗
M
a
t
e
r
i
a
l
d
i
f
f
u
s
e
∗
c
o
s
(
θ
)
I_{diffuse}=Light_{diffuse}*Material_{diffuse}* cos(θ)
Idiffuse=Lightdiffuse∗Materialdiffuse∗cos(θ)
与上面计算相同,实际计算中所用到的是红、绿、蓝分量。
确定入射角 θ 需要:
(a)求解从所绘制向量到光源的向量L——光照向量;
(b)求解所渲染物体表面的法向量N——顶点法向量。
向量 L 可以通过对光照方向向量取反,或通过计算像素位置到光源位置的向量得到。 计算向量 N 会麻烦一些——法向量有可能已经在模型中给出了,但是如果模型没有给出法向量 N,那么就需要基于周围顶点位置,在几何上对向量 N 进行估计。
光照向量L的计算——[向量]减法:
事实上,在计算法向量时,没必要计算出 θ 角本身的角度。我们真正需要的是 cos(θ)。
I
d
i
f
f
u
s
e
=
L
i
g
h
t
d
i
f
f
u
s
e
∗
M
a
t
e
r
i
a
l
d
i
f
f
u
s
e
∗
(
N
^
⋅
L
^
)
I_{diffuse}=Light_{diffuse}*Material_{diffuse}* (\hat { N } \cdot \hat { L })
Idiffuse=Lightdiffuse∗Materialdiffuse∗(N^⋅L^)
漫反射分量仅当表面暴露在光照中时起作用,即要满足θ∈(−90°, 90°),cos(θ) > 0。所以:
I
d
i
f
f
u
s
e
=
L
i
g
h
t
d
i
f
f
u
s
e
∗
M
a
t
e
r
i
a
l
d
i
f
f
u
s
e
∗
m
a
x
(
(
N
^
⋅
L
^
)
,
0
)
I_{diffuse}=Light_{diffuse}*Material_{diffuse}* max((\hat { N } \cdot \hat { L }),\quad0)
Idiffuse=Lightdiffuse∗Materialdiffuse∗max((N^⋅L^),0)
❸镜面反射分量
Ispec决定所渲染的像素是否需要作为“镜面高光”的一部分变亮。它不止与光源的入射角相关,也与光在表面上的反射角以及观察点与反光表面之间的夹角相关。
记忆要点:上图中所有的方向向量都是以像素为原点向外发射开的。
在相机空间中,眼睛位于原点。因为转换到相机空间后,就是以相机(眼睛)为坐标系中心了,所以眼睛当然在原点(是相机空间中的原点)。
在上图中, R——反射光向量(代表反射光的方向), V——视觉向量(也叫观察向量,是从像素到眼睛的向量)。
注意,V 是对从眼睛到像素的向量取反。在 R 与 V 之间的小夹角 φ 越小,眼睛越靠近光轴,或者说看向反射光,因此像素的镜面高光分量也就越大(像素看来应该更亮)。
φ 用于计算镜面反射分量的方式取决于所渲染物体的“光泽度”。极端闪亮的物体,如镜子,其镜面高光非常小——它们将入射的光直接反射给了眼睛。不那么闪亮的物体,其镜面高光会扩散开来,因此高光会包含更多的像素。
反光度
即反射光的强度,通常用衰减函数
来建模,这个衰减函数用来表达随着角度 φ 的增大,镜面反射分量降低到 0 的速度。我们可以用 cos(φ)来对衰减进行建模,通过余弦函数的乘方来增减反光度,如 cos(φ), cos2(φ), cos3(φ), cos10(φ), cos50(φ)等。
如下图所示,可以看到,指数中的阶数越高,衰减越快,因此在视角光轴外的反光像素镜面反射分量越小。我们将衰减函数 cosn(φ)中的指数 n 叫作材质的反光度因子
(上面材质图的最右列给出的“光泽”就是这个反光度因子)。
给出完整的镜面反射计算:
I
s
p
e
c
=
L
i
g
h
t
s
p
e
c
∗
M
a
t
e
r
i
a
l
s
p
e
c
∗
m
a
x
(
0
,
(
R
^
⋅
V
^
)
n
)
I_{spec}=Light_{spec}*Material_{spec}*max(0,\quad(\hat { R } \cdot \hat { V })^n)
Ispec=Lightspec∗Materialspec∗max(0,(R^⋅V^)n)
如之前一样,真正的计算中包含了红、绿、蓝 3 个分量。
注:我们需要确保镜面反射分量不使用 cos(φ) 所产生的负值,如果使用了负值,则会有奇怪的伪影,如“暗”镜面高光。
公式汇总
⒈反射强度Iobserved:
I
o
b
s
e
r
v
e
d
=
I
a
m
b
i
e
n
t
+
I
d
i
f
f
u
s
e
+
I
s
p
e
c
u
l
a
r
I_{observed} = I_{ambient} + I_{diffuse}+I_{specular}
Iobserved=Iambient+Idiffuse+Ispecular
⒉环境光分量Iambient:
I
a
m
b
i
e
n
t
=
L
i
g
h
t
a
m
b
i
e
n
t
∗
M
a
t
e
r
i
a
l
a
m
b
i
e
n
t
I_{ambient}=Light_{ambient}*Material_{ambient}
Iambient=Lightambient∗Materialambient
⒊漫反射分量Idiffuse:
I
d
i
f
f
u
s
e
=
L
i
g
h
t
d
i
f
f
u
s
e
∗
M
a
t
e
r
i
a
l
d
i
f
f
u
s
e
∗
m
a
x
(
(
N
^
⋅
L
^
)
,
0
)
I_{diffuse}=Light_{diffuse}*Material_{diffuse}* max((\hat { N } \cdot \hat { L }),\quad0)
Idiffuse=Lightdiffuse∗Materialdiffuse∗max((N^⋅L^),0)
⒋镜面反射分量Ispec:
I
s
p
e
c
=
L
i
g
h
t
s
p
e
c
∗
M
a
t
e
r
i
a
l
s
p
e
c
∗
m
a
x
(
(
R
^
⋅
V
^
)
n
,
0
)
其
中
,
m
a
x
(
(
R
^
⋅
V
^
)
n
,
0
)
=
=
m
a
x
(
(
R
^
⋅
V
^
)
,
0
)
n
I_{spec}=Light_{spec}*Material_{spec}*max((\hat { R } \cdot \hat { V })^n,\quad0)\\ 其中,max((\hat { R } \cdot \hat { V })^n,\quad0)==max((\hat { R } \cdot \hat { V }),\quad0)^n
Ispec=Lightspec∗Materialspec∗max((R^⋅V^)n,0)其中,max((R^⋅V^)n,0)==max((R^⋅V^),0)n
真正的计算中包含了红、绿、蓝 3 个分量。如镜面反射:
I
s
p
e
c
r
e
d
=
L
i
g
h
t
s
p
e
c
r
e
d
∗
M
a
t
e
r
i
a
l
s
p
e
c
r
e
d
∗
m
a
x
(
(
R
^
⋅
V
^
)
n
,
0
)
▫
I
s
p
e
c
g
r
e
e
n
=
L
i
g
h
t
s
p
e
c
g
r
e
e
n
∗
M
a
t
e
r
i
a
l
s
p
e
c
g
r
e
e
n
∗
m
a
x
(
(
R
^
⋅
V
^
)
n
,
0
)
▫
I
s
p
e
c
b
l
u
e
=
L
i
g
h
t
s
p
e
c
b
l
u
e
∗
M
a
t
e
r
i
a
l
s
p
e
c
b
l
u
e
∗
m
a
x
(
(
R
^
⋅
V
^
)
n
,
0
)
I _ { spec} ^ { red } = Light_{spec}^{red}*Material_{spec}^{red}*max((\hat { R } \cdot \hat { V })^n,\quad0) \\▫ \\ I _ { spec} ^ { green } = Light_{spec}^{green}*Material_{spec}^{green}*max((\hat { R } \cdot \hat { V })^n,\quad0) \\▫ \\ I _ { spec} ^ { blue} = Light_{spec}^{blue}*Material_{spec}^{blue}*max((\hat { R } \cdot \hat { V })^n,\quad0)
Ispecred=Lightspecred∗Materialspecred∗max((R^⋅V^)n,0)▫Ispecgreen=Lightspecgreen∗Materialspecgreen∗max((R^⋅V^)n,0)▫Ispecblue=Lightspecblue∗Materialspecblue∗max((R^⋅V^)n,0)
光颜色加权——[相乘]与[相加]的数学意义
【颜色相乘】称为“调制(Modulate)”,表示的是颜色的混合
。两个相乘的颜色通常是纹理颜色和光线颜色,相乘后得到的最终渲染的颜色。颜色[相乘]的目的是为了模拟光照射到物体上的效果。(例如,一束光线照射到地面上的一张漫反射纹理,那么两个颜色需要相乘,才会产生正确光照射物体的效果。)
【颜色相加】是指光的叠加
,物理上是光的強度相加。颜色相加的目的是为了给光源制造强光效果。(例如你有个灯笼,有一张漫反射纹理,一个发光纹理,那么这两个纹理颜色就要相加,才能产生灯笼发光效果。)
其实从最终的计算效果来看,因为颜色取值在[0,1]区间,所以乘法的值是向0这个方向靠近的,乘法产生的值一般都是偏小的,会相互抵消原先的一些颜色。而加法的值都是向1这个方向靠近的,也就是向白色方向靠近,越趋近白色也就表示越亮。
Color1 = vec4(red1, green1, blue1, alpha1);
Color2 = vec4(red2, green2, blue2, alpha2);
Color1 * Color2 = vec4(red1*red2, green1*green2, blue1*blue2, alpha1*alpha2);// (各分量趋于0)各分量都分别叠加
Color1 + Color2 = vec4(red1+red2, green1+green2, blue1+blue2, alpha1+alpha2);// (各分量趋于1)各分量都分别加强
当相加加权时,如果值大于1,OpenGL会将值限制为1。
生活中的【颜色相乘】案例
光线照射到C色物体上,C色物体将会吸收除C色以外的所有颜色,最终反射光的颜色是光线颜色中没有被吸收的光。我们看到的是反射光的颜色。
这样理解更容易:X色物体只反射光线里面的X色光分量,其他颜色分量被物体吸收。
例1:红光照到蓝色物体上,它不反射光,因为蓝色物体能够吸收除了蓝色以外的颜色,红光被吸收了,所以是黑色。
例2:白光照到蓝色物体上,它只反射蓝光,因为其他颜色都被吸收了。
例3:X色光照到黑色物体上,它不反射光,因为黑色物体能吸收所有光,所以X色光被吸收了,所以是黑色。
例4:X色光照到白色物体上,它反射X色光,因为白光不能吸收所有光,所以是什么光就反射什么光颜色。
下面用数学运算互相佐证上述说法:(向量乘法——各分量相乘)
例1:红vec4(1,0,0,1) * 蓝vec4(0,0,1,1) = vec4(0,0,0,1)——黑色
例2:白vec4(1,1,1,1) * 蓝vec4(0,0,1,1) = vec4(0,0,1,1)——蓝光
例3:Xvec4(r,g,b,1) * 黑vec4(0,0,0,1) = vec4(0,0,0,1)——黑色
例4:Xvec4(r,g,b,1) * 白vec4(1,1,1,1) = vec4(r,g,b,1)——rgb色光
可以推论:X色光线照射到Y色材质上,最终反射光颜色为各分量颜色相乘后组成的颜色X*Y(颜色的混合):
X*Y = vec4(rx,gx,bx,1) * vec4(ry,gy,by,1) = vec4(rxry, gxgy, bxby, 1)
对于定向光和材质而言,是定向光照射到材质上,所以满足上面颜色相乘的推论。求这时反射到眼睛的环境光颜色,那就是定向光颜色和材质环境光特性颜色的混合,用颜色的相乘(环境光无方向性,所以无需乘衰减因子):
I a m b i e n t = L i g h t a m b i e n t ∗ M a t e r i a l a m b i e n t I_{ambient}=Light_{ambient}*Material_{ambient} Iambient=Lightambient∗Materialambient
生活中的【颜色相加】案例
比如有两个手电筒,一支手机筒电量20%,另一支电量50%,照射出的光亮度都偏暗,但如果把这两个手电筒都照向同一位置,亮度瞬间变强,和70%电量状态下单个手电筒照射的亮度一样了。即,两种光相互加强了。
不同于上面的颜色相乘:[相乘]是光照射到物体上的颜色混合效果;[相加]不存在谁照射谁,都是相互独立的,它们是颜色的叠加,是互相加强的。
20%白色vec4(0.2,0.2,0.2,1) + 50%白色vec4(0.5,0.5,0.5,1) = 70%白色vec4(0.7,0.7,0.7,1)——光强增加
同样,20%红色vec4(0.2,0,0,1) + 50%红色vec4(0.5,0,0,1) = 70%红色vec4(0.7,0,0,1)——光强增加
同样,红色vec4(1,0,0,1) + 绿色vec4(0,1,0,1) = 黄色vec4(1,1,0,1)——光强增加
同样,红色vec4(1,0,0,1) + 绿色vec4(0,1,0,1) + 蓝色vec4(0,0,1,1) = 白色vec4(1,1,1,1)——光强增加
上面例子是两种光相互加强;同样对于三种光:环境反射光、漫反射光 和 镜面反射光,这三者之间是相互独立的光,所以不存在混合,存在的是互相加强,所以总光强就是:
I o b s e r v e d = I a m b i e n t + I d i f f u s e + I s p e c u l a r I_{observed} = I_{ambient} + I_{diffuse}+I_{specular} Iobserved=Iambient+Idiffuse+Ispecular
根据以上分析,对于环境光分量而言,有个全局环境光globalAmbient,还有光源环境光特性light.ambient以及材质环境光特性material.ambient;
那么 [混合] 并 [加强] 后的环境光就是:[相乘] 并 [相加]
ambient = globalAmbient * material.ambient + light.ambient * material.ambient;
对于漫反射分量、镜面反射分量原理是一样的,只不过对漫反射分量还有一个光线与物体表面角度形成的衰减因子的值、对镜面反射分量有一个角度还有个物体闪亮因子组成的衰减因子值:
diffuse = light.diffuse * material.diffuse * max(cosTheta, 0.0);
specular = light.specular * material.specular * pow(max(cosPhi, 0.0), material.shininess);
以上是光的颜色与材质颜色的混合,所以用是【相乘】。
最后呈现出的效果,是要三者分量【相加】的,因为三者是相互独立的,通过叠加各分量,给光源制造强光效果,得到这个像素上最终的
颜色:fragColor = ambient + diffuse + specular;
一个顶点多个法向量的情况
如上图,顶点P参与构建三个三角形面,对应三个法向量。当光线照射到点P时,是使用的哪个法向量呢?
其实,最终点P是要经过光栅化的,△PQM上离点P最近的的像素P1的法向量是经光栅化插值产生的,△PMN上离点P最近的的像素P2的法向量也是经光栅化插值产生的。假设光线同时照射到△PQM和△PMN这两个面上,点P1和P2处的光照效果没任何问题,问题是点P同时参与△PQM和△PMN这两个面,那点P处的效果会是怎样呢?两三角面的边界线PM上的效果是怎样的呢?我个人认为,点P及线段PM上的点到底使用哪个法向量是随机的,因为毕竟点P或线段PM只是一个或一条像素点而已,完全不会影响全局的效果;但对于上图的情况,点P必须有三个法向量,因为这三个法向量会影响对应面的其他像素点的插值法向量。
实现ADS光照
默认情况——面片着色(Gouraud着色)
面片着色:我们假定所渲染图元(如多边形或三角形)中每个像素的光照值都一样。因此我们只需要对模型每个多边形的【一个】顶点进行光照计算,然后以每个多边形或每个三角形为基础,将计算结果的光照值【复制到相邻的】像素中。
现在面片着色几乎已经不再使用,因为其渲染结果看来不够真实,同时现代硬件已经可以进行更加精确的计算了。下图展示了一个面片着色环面的例子,其中每个三角形都作为平坦的反射表面。
Phong反射模型
Phong着色:
相比Gouraud着色而言,Phong光照计算是按像素而非顶点完成。由于光照计算需要法向量 N 和光照向量 L,但在模型中仅顶点包含这些信息,因此 Phong 着色通常使用巧妙的“技巧”来实现,其中 N 和 L 在顶点着色器中进行计算,并在光栅化期间插值。下图概述了此策略:
法向量插值的效果如下图:
一个Phong着色圆环示例:
main.cpp
...
#define numVAOs 1
#define numVBOs 4
float cameraX, cameraY, cameraZ;
float torLocX, torLocY, torLocZ;
GLuint renderingProgram;
GLuint vao[numVAOs];
GLuint vbo[numVBOs];
Torus myTorus(0.5f, 0.2f, 48);
int numTorusVertices = myTorus.getNumVertices();
int numTorusIndices = myTorus.getNumIndices();
// 初始化光照位置
glm::vec3 lightLoc = glm::vec3(5.0f, 2.0f, 2.0f);
float amt = 0.0f;
// variable allocation for display
GLuint mvLoc, projLoc, nLoc;
GLuint globalAmbLoc, ambLoc, diffLoc, specLoc, posLoc, mambLoc, mdiffLoc, mspecLoc, mshiLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, mMat, mvMat, invTrMat, rMat;
glm::vec3 currentLightPos, transformed;
float lightPos[3];
// 全局环境光
float globalAmbient[4] = { 0.7f, 0.7f, 0.7f, 1.0f };// 光特征:70%白光
// 白定向光
float dirLightAmbient[4] = { 0.0f, 0.0f, 0.0f, 1.0f };// 光中环境光特征:无
float dirLightDiffuse[4] = { 1.0f, 1.0f, 1.0f, 1.0f };// 光中漫反射光特征:100%白光
float dirLightSpecular[4] = { 1.0f, 1.0f, 1.0f, 1.0f };// 光中镜面反射光特征:100%白光
// 黄金材质
float* matAmb = Util::goldAmbient();
float* matDif = Util::goldDiffuse();
float* matSpe = Util::goldSpecular();
float matShi = Util::goldShininess();
void installLights(glm::mat4 vMatrix) {
// 光源在[相机空间]中的位置
transformed = glm::vec3(vMatrix * glm::vec4(currentLightPos, 1.0));
lightPos[0] = transformed.x;
lightPos[1] = transformed.y;
lightPos[2] = transformed.z;
// get the locations of the light and material fields in the 【shader】
globalAmbLoc = glGetUniformLocation(renderingProgram, "globalAmbient");
ambLoc = glGetUniformLocation(renderingProgram, "light.ambient");
diffLoc = glGetUniformLocation(renderingProgram, "light.diffuse");
specLoc = glGetUniformLocation(renderingProgram, "light.specular");
posLoc = glGetUniformLocation(renderingProgram, "light.position");
mambLoc = glGetUniformLocation(renderingProgram, "material.ambient");
mdiffLoc = glGetUniformLocation(renderingProgram, "material.diffuse");
mspecLoc = glGetUniformLocation(renderingProgram, "material.specular");
mshiLoc = glGetUniformLocation(renderingProgram, "material.shininess");
// set the uniform light and material values in the shader
glProgramUniform4fv(renderingProgram, globalAmbLoc, 1, globalAmbient);// 全局环境光
glProgramUniform4fv(renderingProgram, ambLoc, 1, dirLightAmbient);// 白定向光特征
glProgramUniform4fv(renderingProgram, diffLoc, 1, dirLightDiffuse);
glProgramUniform4fv(renderingProgram, specLoc, 1, dirLightSpecular);
glProgramUniform3fv(renderingProgram, posLoc, 1, lightPos);// 光源在[相机空间]中的位置
glProgramUniform4fv(renderingProgram, mambLoc, 1, matAmb);// 材质特征
glProgramUniform4fv(renderingProgram, mdiffLoc, 1, matDif);
glProgramUniform4fv(renderingProgram, mspecLoc, 1, matSpe);
glProgramUniform1f(renderingProgram, mshiLoc, matShi);
}
void setupVertices(void) {
std::vector<int> ind = myTorus.getIndices();
std::vector<glm::vec3> vert = myTorus.getVertices();
std::vector<glm::vec2> tex = myTorus.getTexCoords();
std::vector<glm::vec3> norm = myTorus.getNormals();
std::vector<float> pvalues;
std::vector<float> tvalues;
std::vector<float> nvalues;
for (int i = 0; i < myTorus.getNumVertices(); i++) {
pvalues.push_back(vert[i].x);
pvalues.push_back(vert[i].y);
pvalues.push_back(vert[i].z);
tvalues.push_back(tex[i].s);
tvalues.push_back(tex[i].t);
nvalues.push_back(norm[i].x);
nvalues.push_back(norm[i].y);
nvalues.push_back(norm[i].z);
}
glGenVertexArrays(1, vao);
glBindVertexArray(vao[0]);
glGenBuffers(numVBOs, vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);// 顶点坐标缓冲区
glBufferData(GL_ARRAY_BUFFER, pvalues.size() * 4, &pvalues[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);// 纹理坐标缓冲区
glBufferData(GL_ARRAY_BUFFER, tvalues.size() * 4, &tvalues[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);// 法向量缓冲区
glBufferData(GL_ARRAY_BUFFER, nvalues.size() * 4, &nvalues[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[3]);// 索引缓冲区
glBufferData(GL_ELEMENT_ARRAY_BUFFER, ind.size() * 4, &ind[0], GL_STATIC_DRAW);
}
void init(GLFWwindow* window) {
renderingProgram = Util::createShaderProgram("vertShader.glsl", "fragShader.glsl");
cameraX = 0.0f; cameraY = 0.0f; cameraZ = 1.0f;
torLocX = 0.0f; torLocY = 0.0f; torLocZ = -1.0f;
glfwGetFramebufferSize(window, &width, &height);
aspect = (float)width / (float)height;
pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
setupVertices();
}
void display(GLFWwindow* window, double currentTime) {
glClear(GL_DEPTH_BUFFER_BIT);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(renderingProgram);
mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix");
projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");
nLoc = glGetUniformLocation(renderingProgram, "norm_matrix");
vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));
mMat = glm::translate(glm::mat4(1.0f), glm::vec3(torLocX, torLocY, torLocZ));
// 圆环绕世界空间X轴固定旋转35°(只是每次都固定旋转固定的角度,不会有动画),
// 使环面更容易看到(因为圆环先经过了平移变换,不在原点了,所以不是自转)。
mMat *= glm::rotate(mMat, toRadians(35.0f), glm::vec3(1.0f, 0.0f, 0.0f));
currentLightPos = glm::vec3(lightLoc.x, lightLoc.y, lightLoc.z);
amt += 0.5f;// 每帧旋转角增加0.5°
rMat = glm::rotate(glm::mat4(1.0f), toRadians(amt), glm::vec3(0.0f, 0.0f, 1.0f));
// 将光源绕世界空间Z轴旋转(光源位置不在原点,所以不是自转)
currentLightPos = glm::vec3(rMat * glm::vec4(currentLightPos, 1.0f));
installLights(vMat);// 准备好光源与材质的一些工作
mvMat = vMat * mMat;
invTrMat = glm::transpose(glm::inverse(mvMat));// 生成【MV矩阵】的[逆转置矩阵]
glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
glUniformMatrix4fv(nLoc, 1, GL_FALSE, glm::value_ptr(invTrMat));
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(1);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[3]);
glDrawElements(GL_TRIANGLES, numTorusIndices, GL_UNSIGNED_INT, 0);
}
int main(void) {
...
}
vertShader.glsl
#version 430
layout (location = 0) in vec3 vertPos;
layout (location = 1) in vec3 vertNormal;
out vec3 varyingNormal;
out vec3 varyingLightDir;
out vec3 varyingVertPos;
struct PositionalLight {
vec4 ambient;
vec4 diffuse;
vec4 specular;
vec3 position;
};
struct Material {
vec4 ambient;
vec4 diffuse;
vec4 specular;
float shininess;
};
uniform PositionalLight light;
uniform Material material;
uniform vec4 globalAmbient;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;
void main(void) {
// 将顶点位置转换到[相机空间]
varyingVertPos = (mv_matrix * vec4(vertPos, 1.0)).xyz;
// 计算[相机空间]中的光照向量(light.position已经在C++程序中转换到[相机空间]了)
varyingLightDir = light.position - varyingVertPos;
// 将法向量转换到[相机空间]
varyingNormal = (norm_matrix * vec4(vertNormal, 1.0)).xyz;
// 正常输出经模型-视图矩阵、透视投影矩阵变换后的顶点坐标
gl_Position = proj_matrix * mv_matrix * vec4(vertPos, 1.0);
}
fragShader.glsl
#version 430
in vec3 varyingNormal;
in vec3 varyingLightDir;
in vec3 varyingVertPos;
out vec4 fragColor;
struct PositionalLight {
vec4 ambient;
vec4 diffuse;
vec4 specular;
vec3 position;
};
struct Material {
vec4 ambient;
vec4 diffuse;
vec4 specular;
float shininess;
};
uniform PositionalLight light;
uniform Material material;
uniform vec4 globalAmbient;
void main(void) {
// 归一化光照向量L、法向量N、视觉向量V(眼睛在原点)
vec3 L = normalize(varyingLightDir);
vec3 N = normalize(varyingNormal);
vec3 V = normalize(-varyingVertPos);
// 计算光照向量L基于表面法向量N的反射光向量,并归一化
vec3 R = normalize(reflect(-L, N));
// 计算光照向量与法向量间的夹角余弦cosθ
float cosTheta = dot(L, N);
// 计算视觉向量与反射光向量的夹角余弦cosφ
float cosPhi = dot(V, R);
// 环境光、漫反射和镜面反射分量(单个像素)
vec3 ambient = ((globalAmbient * material.ambient) + (light.ambient * material.ambient)).xyz;
vec3 diffuse = light.diffuse.xyz * material.diffuse.xyz * max(cosTheta, 0.0);
vec3 specular = light.specular.xyz * material.specular.xyz * pow(max(cosPhi, 0.0), material.shininess);
// 反射强度是ADS三分量之和
fragColor = vec4((ambient + diffuse + specular), 1.0);
}
我们在GLSL代码中看到:
归一化函数normalize()、计算一个向量基于另一个向量的反射reflect()、向量点乘dot()、取最大值max()、求指数Xy=pow(X, y)、vec4.xyz == vec3(vec4)。struct关键字,能很好地分门别类归整一些统一变量。
运行效果如下图:
通过上面动图效果,可以看到,圆环绕X轴固定旋转了35°度角;光源在绕Z轴以点(0,0,2)为圆心,半径为5,以角度0.5°递增旋转。当光源旋转到圆环侧边及上方和下方时,可明显看到圆环表面亮暗的变化。
C++代码中光源为什么是【先】在世界空间中进行旋转矩阵变换(绕Z轴以角度0.5°递增旋转),【然后】才是将光源位置转换到相机空间?
在C++程序中,圆环还没有转换到相机空间(圆环模型转换到相机空间是在顶点着色器程序里进行的),光源和圆环此时都在世界空间,而光源又不是一个模型,得单独处理。
如果先将光源转换到相机空间呢?这样要旋转光源,就得以相机坐标系来进行,要转换坐标系,显然是自找麻烦。况且之前所有例子都没这么干的!对于圆环模型的变换是用这段代码进行的:gl_Position = proj_matrix * mv_matrix * vec4(vertPos, 1.0); 其中mv_matrix=V*M,而这个M就是众多如平移、缩放、旋转变换矩阵的合并,这都是在C++程序中完成的,然后通过统一变量将生成的MV矩阵传递到顶点着色器,最终完成模型的矩阵变换(平移/缩放/旋转)、转换到相机空间、进行透视投影变换。
而对于本例,为什么要在C++代码中而不是在着色器中完成光源转换到相机空间的变换呢?首先,光源不是模型,是没有透视效果的,不需要对光源进行proj_matrix矩阵变换;再一个,上面那段代码中mv_matrix=V*M,其中M是对圆环模型的各种变换,而非光源,所以不能对光源进行mv_matrix变换。那就只能把光源位置直接乘视图矩阵V,得到光源在相机空间中的位置,这个相机空间中的光源位置还会通过统一变量传递到顶点着色器,用于计算在相机空间光照向量及相关角度。某一帧调用display(),光源位置就在绕世界空间Z轴旋转的第N×0.5°处,然后光源位置转换到相机空间,这样就呈现出在相机空间中光源旋转一定角度的位置。这样,每帧都调用display()后,光源位置就在相机空间中产生了旋转动画。
最后,先在世界空间坐标系下进行圆环的建模,然后放置光源位置,那么这个相对位置是不变的(所有值的大小也是不变的)。所以,模型和光源位置在世界空间完成变换,然后统一做视图变换,就都转到相机空间了。并且在世界空间中,做变换很容易找到物体的相对位置,所以在容易的环境(世界空间)下作变换,只要最后乘视图矩阵,都转到相机空间就完事了。
genType reflect(genType I, genType N);
计算入射向量的反射向量。
I - 指定入射向量;
N - 指定法向量。
反射方向的计算为:I - 2.0 * dot(N, I) * N (其中N必须是归一化的)。
(此公式的推导,详见Blog:✠OpenGL-9-天空和背景——关于reflect函数)
如下图,Rin是入射向量,N是单位法向量;经公式计算后,Rreflect就是最终的反射向量。
我们的上面片段着色器程序中有这段代码:vec3 R = normalize(reflect(-L, N));
结合上图,reflect()函数第一个参数 -L 对应Rin,第二个参数N是归一化的表面法向量,式子返回的 R 变量就对应Rreflect。
我们还知道,代码中的写法与之前给出的公式及图片标示(如下)是完全一致的。
所以,只需要按图示求出各向量,注意向量的方向一定要一致,虽然名称听起来好像不应该,但应该自己以图示为标准名称叫法,当然包括其方向了。然后按公式计算即可。
由于我们给的白光是定向光,而定向光是没有位置只有方向的,所以示例中的白光位置只影响光的方向,所以也就没用到衰减因子attenuationFactor。
还要注意,在相机空间中,眼睛是在原点的。
我们在代码中将顶点位置转换到相机空间、计算相机空间中的光照向量、将法向量转换到相机空间,这些都是以[相机空间]为基础空间进行操作的。
可以简单求出:光照向量L = 光源位置向量 - 顶点位置向量。
但要注意到:[相机空间]中的光照向量 = [相机空间]中的光源位置向量 - [相机空间]中的顶点位置向量。
我们只需要用点乘计算出cosθ、cosφ,而无需计算出角度θ、φ。
法向量变换用到『逆转置矩阵』
这里用到了逆转置矩阵:
invTrMat = glm::transpose(glm::inverse(mvMat));
关于逆转置矩阵,在Blog“✠OpenGL-3-数学基础”的补充说明里面有描述:
要对向量 V 使用变换矩阵 M 进行与点相同的变换,一般需要计算 M 的逆转置矩阵,记为(M-1)T,并用所得矩阵乘以 V。
在某些情况下, M=(M-1)T,在这些情况下只要用 M 就可以了。
在C++中用GLM函数计算出MV矩阵的逆转置矩阵,并以统一标识名"norm_matrix"传递到GLSL代码中:
layout (location = 1) in vec3 vertNormal;
我们要对法向量vec3使用与点vec3相同的变换(在相机空间中),所以得先求出MV的逆转置矩阵,然后用法向量矩阵乘以这个逆转置矩阵,从而将法向量转换到相机空间。
Blinn-Phong反射模型
虽然 Phong 着色有着比 Gouraud 着色更真实的效果, 但这是建立在增大性能消耗的基础上的。James Blinn提出了一种对于 Phong 着色的优化方法,被称为 Blinn-Phong 反射模型。这种优化是基于观察到 Phong 着色中消耗最大的计算之一是解出反射向量R。
向量 R 在计算过程中并不是必需的——R 只是用来计算角 φ 的手段。角 φ 的计算可以不用向量 R,而通过 L 与 V 的角平分线向量 H 得到。如左下图所示,H 和 N 之间的角 α 刚好等于 1⁄2(φ)。虽然 α 与 φ 不同,但 Blinn 展示了使用 α 代替 φ 就已经可以获得足够好的结果。
角平分线向量可以简单地使用 L+V 得到(见右下图),之后 cos(α)可以通过 H•N点积计算。
这些计算可以在[片段着色器]中进行,甚至为了性能考虑(经过一些调整)也可以在[顶点着色器中进行]。
用Blinn-Phong 着色。 C++ / OpenGL 代码与之前一样没有变化。
顶点着色器:
...
// 角平分线向量H作为新增的输出
out vec3 varyingHalfVector;
...
void main() {
// 与之前计算相同,增加了L+V的计算
varyingHalfVector = (varyingLightDir + (-varyingVertPos)).xyz;
// 其余顶点着色器代码没有改动
}
片段着色器:
...
in vec3 varyingHalfVector;
...
void main() {
// 现在已经不需要计算反射向量R了
vec3 L、V、N;// 与之前一样
vec3 H = normalize(varyingHalfVector);// New!
. . .
// 计算法向量 N 与角平分线向量 H 之间的角度
float cosPhi = dot(H,N);
// 其余片段着色器代码没有改动
}
结合光照与纹理
在光照模型中,都是假设我们使用按 ADS 定义的光源,照亮按 ADS 定义材质的物体。
但是某些对象的表面可能会指定纹理图像。因此,我们需要一种方法来结合采样纹理所得的颜色和光照模型产生的颜色。
我们结合光照和纹理的方式取决于物体的特性以及其纹理的目的。这里有多种情况,其中常见的有:
❶纹理图像很写实地反映了物体真实的表面外观
物体拥有一个简单的纹理,同时我们对它进行光照。实现这种光照的一种简单方法是在片段着色器中完全将材质特性去除掉,之后使用纹理取样所得纹理颜色代替材质的 ADS 值。下面的伪代码展示了这种策略:(材质material不参与计算)
fragColor = textureColor * (ambientLight + diffuseLight) + specularLight
这种策略下,纹理颜色影响了环境光和漫反射分量,而镜面反射颜色仅由光源决定。镜面反射分量仅由光源决定是一种很常见的做法,尤其是对于金属或“闪亮”的表面。
但是,对于不那么闪亮的表面,如织物或未上漆的木材(甚至一小部分金属,如黄金),其镜面高光部分都应当包含物体表明颜色。在这些情况下,之前的策略应该做适当微调:
fragColor = textureColor * (ambientLight + diffuseLight + specularLight)
❷物体同时具有材质和纹理
如银质物体使用纹理为表面添加一些氧化痕迹。在这些情况下,既用到光照又用到材质的标准 ADS 模型就可以与纹理颜色相结合,并加权求和。如:
textureColor = texture(sampler, texCoord)
lightColor = (ambLight * ambMaterial) + (diffLight * diffMaterial) + specLight
fragColor = 0.5 * textureColor + 0.5 * lightColor
这种策略结合了光照、材质、纹理。
❸材质包括了阴影和反射信息(在第 8 章、第 9 章中)
❹有多种光和/或多个纹理
扩展到多个光源以及多种材质的情况。如:
texture1Color = texture(sampler1, texCoord)
texture2Color = texture(sampler2, texCoord)
light1Color = (ambLight1 * ambMaterial) + (diffLight1 * diffMaterial) + specLight1
light2Color = (ambLight2 * ambMaterial) + (diffLight2 * diffMaterial) + specLight2
fragColor = 0.25 * texture1Color + 0.25 * texture2Color + 0.25 * light1Color + 0.25 * light2Color
以下两幅图展示了拥有 UV 映射纹理图像的Studio552海豚,以及NASA航天飞机模型。这两个有纹理的模型都使用了增强后的 Blinn-Phong 光照,没有使用材质,并在镜面高光中仅使用光照进行计算。
在这两幅图中,片段着色器中颜色相关的计算为:
vec4 texColor = texture(sampler, texCoord);
fragColor = texColor * (globalAmbient + lightAmb + lightDiff * max(dot(L,N), 0.0))
+ lightSpec * pow(max(dot(H,N), 0.0), matShininess * 3.0);
注意,计算过程中 fragColor 可能产生大于 1.0 的值。在这种情况下, OpenGL 会将它限制回 1.0。
补充说明
上图所展示的面片着色(Gouraud着色)的环面是通过在顶点着色器和片段着色器中,将“flat”插值限定符
添加到相应的法向量属性声明中得到的。这样会使得光栅器不对所限定的变量进行插值,而是直接将相同的值赋给每个片段(在默认情况下,它会选择三角形第一个顶点上的值)。在 Phong 着色示例代码中,可以通过如下修改实现面片着色:
在顶点着色器中
flat out vec3 varyingNormal;
在片段着色器中
flat in vec3 varyingNormal;
我们还没有讨论的一类很重要的光是分布式光(distributed light)或区域光(area light)
,这种光的光源是一片区域而非一个单点。它在现实世界相对应的例子是通常在办公室或教室中的日光灯管。
由于 Phong 的反射模型在 3D 图形编程中非常普及,通常 Gouraud 着色模型都是在 Phong 反射模型中进行展示。