目录
- 光照模型
- 光源
- 材质
- ADS光照计算
- 实现ADS光照
- Gouraud着色(双线性光强插值法)
- Phong着色
- Blinn-Phong反射模型
- 结合光照与纹理
- 补充说明
光照模型
光照模型(lighting model)
有时也称为着色模型(shading model)
,在着色器编程存在的情况下,这可能有点儿令人困惑。反射模型(reflection model)
这一术语有时又会进一步使表达复杂化。
现在常见的光照模型称为 ADS 模型,因为它们基于标记为 A、 D 和 S 的 3 种类型的反射:
- A:环境光反射(ambient reflection):模拟低级光照,影响场景中的所有物体。
- D:漫反射(diffuse reflection):根据光线的入射角度调整物体亮度。
- S:镜面反射(specular reflection):展示物体的光泽,通过在物体表面上,光线直接地反射到我们的眼睛的位置,策略性地放置适当大小的高光来实现。
使用 ADS 光照模型需要指定用于像素输出的 RGBA 值上因光照而产生的分量。因素包括:
- 光源类型及其环境光反射、漫反射和镜面反射特性;
- 对象材质的环境光反射、漫反射和镜面反射特征;
- 对象材质的“光泽度”;
- 光线照射物体的角度;
- 从中查看场景的角度。
光源
光源有许多类型,每种光源具有不同的特性, 需要通过不同的步骤来模拟其效果。常见光源类型有:
- 全局光(通常称为“全局环境光”,因为它仅包含环境光组件);
- 定向光(或“远距离光”);
- 位置光(或“点光源”);
- 聚光灯。
全局环境光
是最简单的光源类型。它没有光源位置,无论场景中的对象在何处,用于显示对象的每个像素都有着相同的光照。全局环境光照模拟了现实世界中的一种光线现象——光线经过很多次反射,其光源和方向都已经无法确定。全局环境光仅具有环境光反射分量,用 RGBA 值设定;它没有漫反射或镜面反射分量。例如,全局环境光通常被建模为偏暗的白光,全局环境光可以定义如下:
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 };
衰减因子有多种建模方式。其中一种方式是使用恒定衰减、线性衰减和二次方衰减,引入 3 个非负可调参数(分别称为 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,从而使得衰减因子落入[0, 1]区间,并当 d 增大时接近 0。
聚光灯
同时具有位置和方向。其“锥形”效果可以使用 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 类同时继承于实现位置光和定向光的类。
材质
通过指定 4 个值(我们已经熟悉其中 3 个值——环境光反射、漫反射和镜面反射),可以在 ADS 光照模型中模拟材质。第 4 个值叫作光泽
,正如我们将要看到的那样,它被用来为所选材质建立一个合适的镜面高光。目前,许多不同类型的常见材质已经有可直接使用的 ADS 和光泽了。例如,要模拟锡铅合金的效果,可以指定如下值:
float pewterMatAmbient[4] = { .11f, .06f, .11f, 1.0f };
float pewterMatDiffuse[4] = { .43f, .47f, .54f, 1.0f };
float pewterMatSpecular[4] = { .33f, .33f, .52f, 1.0f };
float pewterMatShininess = 9.85f;
一些其他材质的 ADS RGBA 值见图:
ADS光照计算
当我们绘制场景时,每个顶点坐标都会进行变换以将 3D 世界模拟到 2D 屏幕上。每个像素的颜色都是光栅化、纹理贴图以及插值的结果。现在我们需要加入一个新的步骤来调整这些光栅化之后的像素颜色,以便反应场景中的光照和材质。
光颜色加权——[相乘]与[相加]的数学意义
【颜色相乘】称为“调制(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。
我们需要做的基础 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决定所渲染的像素是否需要作为“镜面高光”的一部分变亮。它不止与光源的入射角相关,也与光在表面上的反射角以及观察点与反光表面之间的夹角相关。
记忆要点:上图中所有的方向向量都是以像素为原点向外发射开的。
在相机空间中,眼睛位于原点(原点并不是指(0,0,0)点)。因为转换到相机空间后,就是以相机(眼睛)为坐标系中心了,所以眼睛当然在原点(是相机空间中的原点)。
在上图中,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(φ) 所产生的负值,如果使用了负值,则会有奇怪的伪影,如“暗”镜面高光。
实现ADS光照
“面片着色
”或“平坦着色”。这里我们假定所渲染图元(如多边形或三角形)中每个像素的光照值都一样。因此我们只需要对模型中每个多边形的一个顶点进行光照计算, 然后以每个多边形或每个三角形为基础,将计算结果的光照值复制到相邻的像素中。
现在面片着色几乎已经不再使用,因为其渲染结果看来不够真实,同时现代硬件已经可以进行更加精确的计算了。我们将会研究两种流行的平滑着色
方法: Gouraud 着色和 Phong 着色。
Gouraud着色(双线性光强插值法)
法国计算机科学家 Henri Gouraud 在 1971 年发表的平滑着色算法后来被称为Gouraud着色
。
Gouraud着色过程如下:
(1)确定每个顶点的颜色,并进行光照相关计算。
(2)允许正常的栅格化过程在插入像素时对颜色也进行插值(同时也对光照进行插值)。
在 OpenGL 中,这表示大多数光照计算都是在顶点着色器中完成的,片段着色器仅传递并展示自动插值的光照后的颜色。
在场景中包含环面和单一位置光的情况下在 OpenGL 中实现 Gouraud 着色器的策略及程序如下:
#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 initialLightLoc = 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];
// white light
float globalAmbient[4] = { 0.7f, 0.7f, 0.7f, 1.0f };
float lightAmbient[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
float lightDiffuse[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
float lightSpecular[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
// gold material
float* matAmb = Utils::goldAmbient();
float* matDif = Utils::goldDiffuse();
float* matSpe = Utils::goldSpecular();
float matShi = Utils::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, lightAmbient);
glProgramUniform4fv(renderingProgram, diffLoc, 1, lightDiffuse);
glProgramUniform4fv(renderingProgram, specLoc, 1, lightSpecular);
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) {
cameraX = 0.0f; cameraY = 0.0f; cameraZ = 1.0f;
torLocX = 0.0f; torLocY = 0.0f; torLocZ = -1.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));
// glm::rotate:输入矩阵 mMat 乘以这个旋转矩阵 rotMat,即:mMat=mMat*rotMat
mMat *= glm::rotate(mMat, toRadians(35.0f), glm::vec3(1.0f, 0.0f, 0.0f));
currentLightPos = glm::vec3(initialLightLoc.x, initialLightLoc.y, initialLightLoc.z);
amt += 0.5f;// 每帧旋转角增加0.5°
rMat = glm::rotate(glm::mat4(1.0f), toRadians(amt), glm::vec3(0.0f, 0.0f, 1.0f));
currentLightPos = glm::vec3(rMat * glm::vec4(currentLightPos, 1.0f));
installLights(vMat);
mvMat = vMat * mMat;
invTrMat = glm::transpose(glm::inverse(mvMat));
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);
}
...
// 顶点着色器
#version 430
layout (location = 0) in vec3 vertPos;
layout (location = 1) in vec3 vertNormal;
out vec4 varyingColor;
struct PositionalLight {// 统一块
vec4 ambient;
vec4 diffuse;
vec4 specular;
vec3 position;
};
struct Material {
vec4 ambient;
vec4 diffuse;
vec4 specular;
float shininess;
};
uniform vec4 globalAmbient;
uniform PositionalLight light;
uniform Material material;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;
void main(void) {
vec4 color;
// convert vertex position to view space
vec4 P = mv_matrix * vec4(vertPos,1.0);
// convert normal to view space
vec3 N = normalize((norm_matrix * vec4(vertNormal,1.0)).xyz);
// calculate view-space light vector (from point to light)
vec3 L = normalize(light.position - P.xyz);
// view vector is negative of view space position
vec3 V = normalize(-P.xyz);
// R is reflection of -L around the plane defined by N
vec3 R = reflect(-L,N);
// ambient, diffuse, and specular contributions
vec3 ambient =
((globalAmbient * material.ambient)
+ (light.ambient * material.ambient)).xyz;
vec3 diffuse =
light.diffuse.xyz * material.diffuse.xyz
* max(dot(N,L), 0.0);
vec3 specular =
pow(max(dot(R,V), 0.0f), material.shininess)
* material.specular.xyz * light.specular.xyz;
// send the color output to the fragment shader
varyingColor = vec4((ambient + diffuse + specular), 1.0);
// send the position to the fragment shader, as before
gl_Position = proj_matrix * mv_matrix * vec4(vertPos,1.0);
}
// 片段着色器
#version 430
in vec4 varyingColor;
out vec4 fragColor;
// interpolate lighted color
// (interpolation of gl_Position is automatic)
void main(void) {
fragColor = varyingColor;
}
直接对法向量应用 MV 矩阵不能保证法向量依然与物体表面垂直。正确的变换是运用 MV 矩阵的逆转置矩阵。GLSL 函数 normalize(),它用来将向量转换为单位长度。正确地运用点积运算需要先使用该函数。 reflect()函数则用来计算一个向量基于另一个向量的反射。
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。
输出的环面中有很明显的伪影。其镜面高光有着块状、面片感。这种伪影在物体移动时会更加明显(但我们在书中没法展示移动的物体)。Gouraud 着色容易受到其他伪影影响。如果镜面高光整个范围都在模型中的一个三角形内——高光范围内一个模型顶点也没有,那么它可能不会被渲染出来。
由于镜面反射分量是依顶点计算的,因此,当模型的所有顶点都没有镜面反射分量时,其栅格化后的像素也不会有镜面反射效果。
Phong着色
Bui Tuong Phong 在犹他大学读研究生期间开发了一种平滑的着色算法。该算法的结构类似 Gouraud 着色算法,不同之处在于光照计算是按像素而非顶点完成的。
// 顶点着色器
#version 430
layout (location = 0) in vec3 vertPos;
layout (location = 1) in vec3 vertNormal;
out vec3 varyingNormal;
out vec3 varyingLightDir;
out vec3 varyingVertPos;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;
void main(void) {
varyingVertPos = (mv_matrix * vec4(vertPos,1.0)).xyz;
varyingLightDir = light.position - varyingVertPos;
varyingNormal = (norm_matrix * vec4(vertNormal,1.0)).xyz;
gl_Position = proj_matrix * mv_matrix * vec4(vertPos,1.0);
}
// 片段着色器
#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 vec4 globalAmbient;
uniform PositionalLight light;
uniform Material material;
void main(void) {
// normalize the light, normal, and view vectors:
vec3 L = normalize(varyingLightDir);
vec3 N = normalize(varyingNormal);
vec3 V = normalize(-varyingVertPos);
// compute light reflection vector, with respect N:
vec3 R = normalize(reflect(-L,N));
// get the angle between the light and surface normal:
float cosTheta = dot(L,N);
// angle between the view vector and reflected light:
float cosPhi = dot(V,R);
// compute ADS contributions (per pixel):
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);
fragColor = vec4((ambient + diffuse + specular), 1.0);
}
Blinn-Phong反射模型
James Blinn 在 1977 年提出了一种对于 Phong 着色的优化方法, 称为 Blinn-Phong 反射模型。
虽然 Phong 着色有着比 Gouraud 着色更真实的效果, 但这是建立在增大性能消耗的基础上的。这种优化的依据是,Phong 着色中消耗最大的计算之一是求反射向量 R。
向量 R 在计算过程中并不是必需的——R 只是用来计算角 φ 的手段。角 φ 的计算可以不使用向量 R,而通过 L 与 V 的角平分线向量 H 得到。H 和 N 之间的角 α 刚好等于 1/2(φ)。 虽然 α 与 φ 不同, 但 Blinn 展示了使用 α 代替 φ 就已经可以获得足够好的结果。角平分线向量可以简单地使用 L+V 得到。
// 顶点着色器
...
// 角平分线向量 H 作为新增的输出
out vec3 varyingHalfVector;
...
void main(void) {
// 与之前的计算相同,增加了 L+V 的计算
varyingHalfVector = (varyingLightDir + (-varyingVertPos)).xyz;
// 其余顶点着色器代码没有改动
}
// 片段着色器
...
in vec3 varyingHalfVector;
...
void main(void) {
// 注意,现在已经不需要在片段着色器中计算 R
vec3 N = normalize(varyingNormal);
vec3 H = normalize(varyingHalfVector);
...
// 计算法向量 N 与角平分线向量 H 之间的角度
float cosPhi = dot(H,N);
...
}
结合光照与纹理
我们结合光照和纹理的方式取决于物体的特性及其纹理的目的。这里有多种情况,其中常见的有:
- 纹理图像很写实地反映了物体真实的表面外观;
- 物体同时具有材质和纹理;
- 材质包括阴影和反射信息(在第 8 章、第 9 章中将介绍);
- 有多种光或多个纹理。
我们先来观察第一种情况,物体拥有一个简单的纹理, 同时我们对它进行光照。实现这种光照的一种简单方法是在片段着色器中完全将材质特性去除,之后使用纹理取样所得纹理颜色代替材质的 ADS 值。下面的伪代码展示了这种策略:
// 原本 fragColor = ambient + diffuse + specular
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
这种策略结合了光照、材质、纹理,并能够扩展到多个光源、多种材质的情况。如:
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)
,这种光的光源是一片区域而非一个单点。它在现实世界相对应的例子是通常在办公室或教室中的日光灯管。