计算机图形学与opengl C++版 学习笔记 第7章 光照

7.1 光照模型

光照模型(Lighting model)有时也被称为着色模型(Shading model),有时又使用术语反射模型(Reflection model)现在最常见的光照模型称为“ADS”模型,因为它们基于A、D和S的3种类型的反射。(参考这个)

  • 环境光反射(Ambient reflection)模拟低级光照,影响场景中的所有物体。
  • 漫反射(Diffuse reflection)根据光线的入射角度调整物体亮度。
  • 镜面反射(Specular reflection)用以展示物体的光泽,通过在物体表面上,光线最直接地反射到我们的眼睛的位置,策略性地放置适当大小的高光来实现。

ADS模型可用于模拟不同的光照效果和各种材质。 图7.1(见彩插)展示了位置光对于闪亮黄金环面的环境光反射、漫反射和镜面反射分量。

在这里插入图片描述

图7.1 ADS光照分量
场景的绘制最终是由片段着色器为屏幕上的每个像素输出颜色而实现的。使用ADS光照模型需要指定光照在像素的RGBA输出值上产生的分量。具体包括:
  • 光源类型及其环境、漫反射和镜面反射特性;
  • 对象材质的环境、漫反射和镜面反射特征;
  • 对象的材质指定为“光泽”;
  • 光线照射物体的角度;
  • 从中查看场景的角度。

7.2 光源(光源属性)

常见光源类型有:

  • 全局光(通常称为“全局环境光”,因为它仅包含环境光组件);
  • 定向光(或“远距离光”);
  • 位置光(或“点光源”);
  • 聚光灯。

全局环境光是最简单的光源模型。它没有光源位置——无论场景中的对象在何处,其上的每个像素都有着相同的光照。全局环境光照模拟了现实世界中光线经过很多次反射,其光源和方向都已经无法确定的现象。全局环境光仅具有环境光反射分量,用RGBA 值设定;它没有漫反射或镜面反射分量。例如,全局环境光可以定义如下:

float globalAmbient[4] = {
    
     0.6f, 0.6f, 0.6f, 1.0f };

RGBA的取值范围为0~1,全局环境光通常被建模为偏暗的白光, 其中RGB各值设为0~1的相同的小数,alpha设置为1。

定向光或远距离光也没有源位置,但它具有方向。它可以用来模拟光源距离非常远,以至于光线接近平行的情况,例如阳光。通常在这种情况下,我们可能只对建模光照感兴趣,而对发光的物体不感兴趣。定向光对物体的影响取决于光照角度,物体在朝向定向光的一侧比在切向或相对侧更亮。建模定向光需要指定其方向(以向量形式)及其环境、漫反射和镜面特征(以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)结合进行计算:

在这里插入图片描述
将这个因子与光的强度相乘可以使距光更远时,光的强度衰减更多。注意,kc应当永远设置为大于等于1的值,从而使得衰减因子落入[0…1]区间,并当d增大时接近于0。

聚光灯(spotlight)同时具有位置和方向。其“锥形”效果可以使用0°~90°的截光角θ来模拟,指定光束的半宽度,并使用衰减指数来模拟随光束角度的强度变化。如图7.2所示,我们确定聚光灯方向与从聚光灯到像素的向量之间的角度φ。当φ小于θ时,我们通过将φ的余弦提高到衰减指数来计算强度因子(当φ大于θ时,强度因子设置为0)。结果是强度因子的范围为0~1。衰减指数会影响当角度φ增加时,强度因子趋于0的速率。然后将强度因子乘以光的强度以模拟锥形效果。
在这里插入图片描述

图7.2 聚光灯参数
位于(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类同时继承于实现位置光和定向光的类。在示例中,由于内容足够简单,因此我们在当前版本中没有加入这种层次结构。

7.3 材质(渲染对象属性)

我们场景中物体的“外观”目前仅使用颜色和纹理进行表现。增加的光照使得我们可以加入表面的反射特性。即对象如何与我们的ADS光照模型相互作用。这可以通过将每个对象视为“由某种材质制成”来建模。

通过指定4个值(我们已经熟悉其中3个值——环境光、漫反射和镜面RGB颜色),可以在ADS光照模型中模拟材质。第四种叫作光泽,它被用来为所选材质建立一个合适的镜面高光。目前许多不同类型的常见材质已经有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值见图7.3

有时候一些其他特性也属于材质特性。透明度由RGBA标准中的第四个(alpha)通道的不透明度来实现。取值为1.0是表示完全不透明,取值为0时表示完全透明。对于大多数材质而言,只需要把不透明度设置为1.0就行了,但是对于某些特定的材质,加入一些透明度是很重要的。例如,图7.3中材质“玉”和“珍珠”都含有少量透明度(取值略微小于1.0)以显得更加真实。
在这里插入图片描述

图7.3 其他材质的ADS系数

放射性有时也包含在ADS材质规范中。在模拟自身发光的材质(例如磷光材质)时非常有用。

没有纹理的物体在渲染时,通常需要指定材质特性。因此,预定义一些可供选择的材质,在使用时会很方便。因此我们需要在 Utils.cpp文件中添加如下代码:

// 黄金材质 — 环境光、漫反射、镜面反射和光泽
float * Utils::goldAmbient() {
    
     static float a[4] = {
    
     0.2473f,
0.1995f, 0.0745f, 1 }; return
(float * ) a; }
float * Utils::goldDiffuse() {
    
     static float a[4] = {
    
     0.7516f,
0.6065f, 0.2265f, 1 }; return
(float * ) a; }
float * Utils::goldSpecular() {
    
     static float a[4] = {
    
     0.6283f,
0.5559f, 0.3661f, 1 }; return
(float * ) a; }
float Utils::goldShininess() {
    
     return 51.2f; }

// 白银材质 — 环境光、漫反射、镜面反射和光泽
float * Utils::silverAmbient() {
    
     static float a[4] = {
    
     0.1923f,0.1923f, 0.1923f, 1 }; return
(float * ) a; }
float * Utils::silverDiffuse() {
    
     static float a[4] = {
    
     0.5075f,
0.5075f, 0.5075f, 1 }; return
(float * ) a; }
float * Utils::silverSpecular() {
    
     static float a[4] = {
    
     0.5083f,
0.5083f, 0.5083f, 1 }; return
(float * ) a; }
float Utils::silverShininess() {
    
     return 51.2f; }

// 青铜材质 — 环境光、漫反射、镜面反射和光泽
float * Utils::bronzeAmbient() {
    
     static float a[4] = {
    
     0.2125f,
0.1275f, 0.0540f, 1 }; return
(float * ) a; }
float * Utils::bronzeDiffuse() {
    
     static float a[4] = {
    
     0.7140f,
0.4284f, 0.1814f, 1 }; return
(float * ) a; }
float * Utils::bronzeSpecular() {
    
     static float a[4] = {
    
     0.3936f,
0.2719f, 0.1667f, 1 }; return
(float * ) a; }
float Utils::bronzeShininess() {
    
     return 25.6f; }

这样在init()函数中或全局中为物体指定“黄金”材质就非常容 易了,如下所示。

float* matAmbient = Utils::goldAmbient();
float* matDiffuse = Util::goldDiffuse();
float* matSpecular = util.goldSpecular();
float matShininess = util.goldShininess();

注意,目前为止的各小节中,我们所用来实现的光照和材质特性的代码并没有引入光照。这些代码仅仅提供了用于描述并存储场景中元素所需光照和材质特性的一种方式。我们仍然需要自己计算光照。

编写计算光照的代码需要在我们的着色器代码中引入一些严肃的数学过程。因此,让我们先来看看在C++/OpenGL和GLSL图形程序中实现ADS 光照的基础。

7.4 ADS光照计算(像素属性)

参考这个)当我们绘制场景时,每个顶点坐标都会进行变换以将3D世界模拟到2D屏幕上。每个像素的颜色都是光栅化、纹理贴图以及插值的结果。现在我们需要加入一个新的步骤来调整这些光栅化之后的像素颜色,以便反应场景中的光照和材质。我们需要做的基础ADS计算是确定每个像素的反射强度(Reflection Intensity,I)。计算过程如下:

在这里插入图片描述

我们需要计算每个光源对于每个像素的环境光反射、漫反射和镜面反射分量,并求和。

环境光分量是最简单的。它的值是场景环境光与材质环境光分量的乘积:

在这里插入图片描述

请记住光与材质亮度都是RGB值,计算可以更准确地描述为:

在这里插入图片描述

漫反射分量会更复杂一些,因为它基于光对于平面的入射角。朗伯余弦定律(1760年出版)确定了表面反射的光量与光入射角的余弦成正比。可以建模为如下公式:

在这里插入图片描述

与上面的计算相同,实际计算中所用到的是红、绿、蓝分量。

确定入射角θ需要(a)求解从所绘制向量到光源的向量(或者与光照方向相反的向量),(b)求解所渲染物体表面的法(垂直)向量。让我们将其分别称为L和 N,如图7.4所示。

在这里插入图片描述

图7.4 入射角
基于场景中光的物理特性,向量L可以通过对光照方向向量取反,或通过计算像素位置到光源位置的向量得到。计算向量N会麻烦一些——法向量有可能已经在模型中给出了,但是如果模型没有给出法向量N ,那么就需要基于周围顶点位置,在几何上对向量 N进行估计。在本章剩下的内容中,我们假设所渲染的模型每个顶点都包含法向量(使用建模工具如MAYA或Blender创建的模型,通常都包含法向量)。

事实上,在计算法向量时,没必要计算出θ角本身的角度。我们真正需要的是cos(θ)。在第3章中讲过,这可以通过点乘计算得出。

因此,漫反射分量可以通过如下公式得出:

在这里插入图片描述
漫反射分量仅当表面暴露在光照中时起作用,即当−90 <θ <90,cos(θ) > 0时。因此,我们需要将之前等式的最右项替换为:

在这里插入图片描述

镜面反射分量决定所渲染的像素是否需要作为“镜面高光”的一部分变亮。它不止与光源的入射角θ相关,也与光在表面上的反射角θ以及观察点与反光表面之间的夹角φ相关。

在图7.5中,R代表光反射的方向,V(叫作观察向量view vector)是从像素到眼睛的向量。注意,V是对从眼睛到像素的向量取反(在相机空间中,眼睛位于原点)。在 R与V之间的小夹角φ越小,眼睛越靠近光轴,或者说看向反射光,因此像素的镜面高光分量也就 越大(像素看来应该更亮)。

在这里插入图片描述

图7.5 观察点入射角

φ用于计算镜面反射分量的方式取决于所渲染物体的“光泽度”。极端闪亮的物体,如镜子,其镜面高光非常小——它们将入射的光直接反射给了眼睛。不那么闪亮的物体,其镜面高光会扩散开来,因此高光会包含更多的像素。

反光度通常用衰减函数来建模,这个衰减函数用来表达随着角度φ的增大,镜面反射分量降低到0的速度。我们可以用cos(φ)来对衰减进行建模,通过余弦函数的乘方来增减反光度,如cos(φ),cos2(φ), cos3(φ), cos10(φ), cos50(φ)等,如图7.6所示。

在这里插入图片描述

图7.6 以余弦指数建模的反光度

注意,指数中的阶数越高,衰减越快,因此在视角光轴外的反光像素镜面反射分量越小。我们将衰减函数cosn(φ)中的指数n叫作材质的反光度因子。注意在之前的图7.3中,每个材质的反光度因子在最右列给出。

现在我们可以给出完整的镜面反射计算:
在这里插入图片描述

注意,与之前计算漫反射一样,我们使用了max()函数。在本例中,我们需要确保镜面反射分量不使用cos(φ)所产生的负值,如果使用了负值,则会有奇怪的伪影,如“暗”镜面高光。 同时,如之前一样,真正的计算中包含了红、绿、蓝3个分量。

7.5 实现ADS光照

参考这个)在7.4节中所讲述的计算目前为止都是理论上的,其中包含的假设是,我们可以对每个像素都实行这些操作。但是真实情况会更复杂,

通常模型中只有用来定义模型的顶点才有法向量(N),而非每个像素都有。因此我们要么需要计算每个像素的法向量,这会非常耗时,要么需要使用其他方法对所需的值进行估计,以实现足够好的效果。.

其中一种途径称为“面片着色”或“平坦着色”。这里我们假定所渲染图元(如多边形或三角形)中每个像素的光照值都一样。因此我们只需要对模型每个多边形的一个顶点进行光照计算,然后以每个多边形或每个三角形为基础,将计算结果的光照值复制到相邻的像素中。

现在面片着色几乎已经不再使用,因为其渲染结果看来不够真实,同时现代硬件已经可以进行更加精确的计算了。图7.7中展示了一 个面片着色环面的例子,其中每个三角形都作为平坦的反射表面。
在这里插入图片描述

图7.7 面片着色的环面

虽然某些情况下,面片着色可能已经够用了(或者故意使用其效果),但是通常“平滑着色”是一种更好的途径。在平滑着色的过程中,会对每个像素计算光照强度。现代显卡的并行处理功能,以及 OpenGL图形管线中的插值渲染让平滑着色变得可行。

我们将会观察两个流行的平滑着色方法:Gouraud着色和Phong着 色。

7.5.1 Gouraud着色(双线性光强插值法)

Gouraud着色过程如下。

  1. 确定每个顶点的颜色,以及光照相关计算。
  2. 允许正常的光栅化过程在插入像素时对颜色也进行插值(同时也对光照进行插值)。

在OpenGL中,这表示大多数光照计算都是在顶点着色器中完成的,片段着色器仅做传递并展示自动插值的光照后的颜色。

图7.8展示了在场景中包含环面和单一位置光的情况下,我们将会用来在OpenGL中实现Gouraud着色器的策略。程序7.1中实现了这个策略。

在这里插入图片描述

图7.8 实现Gouraud着色

程序7.1 位置光和Gouraud着色器下的环面

#include <GL\glew.h>
#include <GLFW\glfw3.h>
#include <SOIL2\soil2.h>
#include <string>
#include <iostream>
#include <fstream>
#include <glm\gtc\type_ptr.hpp> // glm::value_ptr
#include <glm\gtc\matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale, glm::perspective
#include "Torus.h"
#include "Utils.h"
using namespace std;

float toRadians(float degrees) {
    
     return (degrees * 2.0f * 3.14159f) / 360.0f; }

#define numVAOs 1
#define numVBOs 4
// 用于创建着色器和渲染程序
GLuint renderingProgram;

// VAO、2个VBO以及环面的声明
GLuint vao[numVAOs];
GLuint vbo[numVBOs];

//环面与相机位置的声明和赋值
float cameraX, cameraY, cameraZ;
float torLocX, torLocY, torLocZ;

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;

// 为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;// 在模型和视觉空间中的光照位置,Vector3f类型
float lightPos[3];// 光照位置的浮点数组

// 白光特性
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 };

// 对象黄金材质特性
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;

	// 在着色器中获取光源位置和材质属性
	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");

	// 在着色器中为光源与材质统一变量赋值
	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) {
    
    
	renderingProgram = Utils::createShaderProgram("./GouraudShaders/vertShader.glsl", "./GouraudShaders/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));
	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;
	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);
	// 通过合并矩阵v和m,创建模型-视图(MV)矩阵,如前
	mvMat = vMat * mMat;
	// 构建MV矩阵的逆转置矩阵,以变换法向量
	invTrMat = glm::transpose(glm::inverse(mvMat));
	// 将MV、PROJ以及逆转置(法向量)矩阵传入相应的统一变量
	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));
	// 在顶点着色器中,将顶点缓冲区(VBO #0)绑定到顶点属性#0
	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
	glEnableVertexAttribArray(0);
	// 在顶点着色器中,将法向缓冲区(VBO #2)绑定到顶点属性#1
	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);
}

void window_size_callback(GLFWwindow* win, int newWidth, int newHeight) {
    
    
	aspect = (float)newWidth / (float)newHeight;
	glViewport(0, 0, newWidth, newHeight);
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
}

int main(void) {
    
    
	if (!glfwInit()) {
    
     exit(EXIT_FAILURE); }
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	GLFWwindow* window = glfwCreateWindow(800, 800, "Chapter7 - program1", NULL, NULL);
	glfwMakeContextCurrent(window);
	if (glewInit() != GLEW_OK) {
    
     exit(EXIT_FAILURE); }
	glfwSwapInterval(1);

	glfwSetWindowSizeCallback(window, window_size_callback);

	init(window);

	while (!glfwWindowShouldClose(window)) {
    
    
		display(window, glfwGetTime());
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwDestroyWindow(window);
	glfwTerminate();
	exit(EXIT_SUCCESS);
}

程序7.1中display()函数与之前程序中的类似,在这里不同的是它同时也将光照和材质信息传入顶点着色器。为了传入这些信息,它调用 installLights(),将光源在视觉空间中的位置,以及材质的ADS特性,读入相应的统一变量以供着色器使用。

其中一个重要的细节是变换矩阵MV,用来将顶点位置移动到视觉空间,但它并不总能正确地将法向量也调整进视觉空间。直接对法向量应用MV矩阵不能保证法向量依然与物体表面垂直。正确的变换是MV 的逆转置矩阵,在第3章“补充说明”中有描述。在程序7.1中,这个新增的矩阵叫作“invTrMat”,通过统一变量传入着色器。

变量lightPosV包含光源在相机空间中的位置。我们每帧只需要计算一次,因此我们在installLights()中[在display()中调用]而非着色器中计算。着色器在下方的续程序7.1中。其中顶点着色器使用了一些我们目前没有见过的符号。注意,在顶点着色器最后进行了向量加法——在第3章中有讲,并且在GLSL中可用。我们将会在展示着色器之后讨论其他符号。

续程序7.1*

vertShader.glsl

#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;
	
	// 将顶点位置转换到视觉空间
	vec4 P = mv_matrix * vec4(vertPos,1.0);
	// Gouraud着色算法
	// 将法向量转换到视觉空间
	vec3 N = normalize((norm_matrix * vec4(vertNormal,1.0)).xyz);//这是将vec4转换为仅包含其前3个元素的等效vec3的快捷方式。
	
	// 计算视觉空间光照向量(从顶点到光源)
	vec3 L = normalize(light.position - P.xyz);
	
	//  视觉向量等于视觉空间中的负顶点位置
	vec3 V = normalize(-P.xyz);
	
	//  R是-L的相对于表面向量N的镜像
	vec3 R = reflect(-L,N);

	// 环境光、漫反射和镜面反射分量
	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;

	// 将颜色输出发送到片段着色器
	varyingColor = vec4((ambient + diffuse + specular), 1.0);
	
	// 将位置发送到片段着色器,如前
	gl_Position = proj_matrix * mv_matrix * vec4(vertPos,1.0);
}

fragShader.glsl

#version 430

in vec4 varyingColor;
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;
uniform mat4 mv_matrix;	 
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;

//  interpolate lighted color
// (interpolation of gl_Position is automatic)

void main(void)
{
    
    	fragColor = varyingColor;
}

程序7.1的输出如图7.9所示。

在这里插入图片描述

图7.9 Gouraud着色的环面

顶点着色器代码中有我们第一次使用了结构体语法的示例。 GLSL“结构体”就像一个数据类型,它有名称和一组字段。当使用结构体名称声明变量时,这个变量将包含结构体中声明的字段,并可以通过“.”语法访问字段。例如,变量“light”声明为 “PositionalLight”类型,因此我们可以在其后引用其字段 light.ambient,light.diffuse等。

还要注意字段选择器符号“.xyz”,我们在顶点着色器中的多个地方都使用了这种语法。这是将vec4转换为仅包含其前3个元素的等效 vec3的快捷方式。

绝大多数光照计算发生在顶点着色器中。对于每个顶点,将适当的矩阵变换应用于顶点位置和相关的法向量,并计算用于光方向(L)和反射(R)的向量。然后执行7.4节中描述的ADS计算,得到每个顶点的颜色(代码中名为varyingColor)。

颜色作为正常光栅化过程的一部分进行插值。之后片段着色器仅作为简单传递。冗长的统一变量声明列表也在片段着色器中(由于前面第4章中描述的原因),但实际上并没有在那里使用它们。

注意GLSL函数normalize(),它用来将向量转换为单位长度。正确地进行点积运算必须要先使用该函数。reflect()函数则计算一个向量基于另一个向量的反射。

图7.9输出的环面中有很明显的伪影。其镜面高光有着块状、面片感。这种伪影在物体移动时会更加明显。

Gouraud着色也容易受到其他伪影影响。如果镜面高光整个范围都在模型中的一个三角形内——即高光范围内一个模型顶点也没有——那么它可能不会被渲染出来。由于镜面反射分量是依顶点计算的,因此,当模型所有顶点都没有镜面反射分量时,其光栅化后的像素也不会有镜面反射光。

7.5.2 Phong着色(法向量插值)

该算法的结构类似于Gouraud着色的算法,其不同之处在于光照计算是按像素而非顶点完成。由于光照计算需要法向量N和光向量L,但在模型中仅顶点包含这些信息,因此Phong着色通常使用巧妙的“技巧”来实现,其中N和L在顶点着色器中进行计算,并在光栅化期间插值。图7.10概述了此策略。

在这里插入图片描述

图7.10 实现Phong着色
C++/OpenGL代码完全如前。之前部分在顶点着色器中完成的过程现在回放入片段着色器中进行。法向量插值的效果如图7.11所示。

在这里插入图片描述

图7.11 法向量插值

现在我们已经准备好使用Phong着色实现位置光照射下的环面了。 大多数代码与实现Gouraud着色的代码相同。由于C++/OpenGL代码完全没有改变,在此我们只展示修改过的顶点着色器和片段着色器,见程序7.2。程序7.2的输出如图7.12所示,Phong着色修正了Gouraud着色中出现的伪影。

在这里插入图片描述

图7.12 Phong着色的环面

程序7.2 Phong着色的环面
main.cpp与程序7.1完全一致 唯一区别在于这里,由

renderingProgram = Utils::createShaderProgram("./GouraudShaders/vertShader.glsl", "./GouraudShaders/fragShader.glsl");

改成

renderingProgram = Utils::createShaderProgram("./PhongShaders/vertShader.glsl", "./PhongShaders/fragShader.glsl");

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 vec4 globalAmbient;
uniform PositionalLight light;
uniform Material material;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;

void main(void)
{
    
    	
	// Phong着色算法
	// 输出顶点位置、光照方向和法向量到光栅器以进行插值
	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);
}

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 vec4 globalAmbient;
uniform PositionalLight light;
uniform Material material;
uniform mat4 mv_matrix;	 
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;

void main(void)
{
    
    	
	// Phong着色算法
	// 正规化光照向量、法向量、视觉向量
	vec3 L = normalize(varyingLightDir);
	vec3 N = normalize(varyingNormal);
	vec3 V = normalize(-varyingVertPos);
	
	// 计算光照向量基于N的反射向量
	vec3 R = normalize(reflect(-L, N));
	
	// 计算光照与平面法向量间的角度
	float cosTheta = dot(L,N);
	
	// 计算视觉向量与反射光向量的角度
	float cosPhi = dot(V,R);

	// 计算ADS分量(按像素),并合并以构建输出颜色
	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);
}

虽然Phong着色有着比Gouraud着色更真实的效果,但这是建立在增大性能消耗的基础上的。James Blinn在1977年提出了一种对于 Phong着色的优化方法[BL77],被称为Blinn-Phong反射模型。这种优化是基于观察到Phong着色中消耗最大的计算之一是解出反射向量 R。

Blinn发现向量R在计算过程中并不是必需的——
R只是用来计算角φ的手段。角φ的计算可以不用向量R,而通过L与V的角平分线向量得到。如图7.13所示,H和N之间的角α刚好等于1⁄2(φ)。虽然α与φ不同,但Blinn展示了使用α代替就已经可以获得足够好的结果。

在这里插入图片描述

图7.13 Blinn-Phong反射

角平分线向量可以简单地使用L+V得到(见图7.14),之后cos(α)可以通过H·N的点积计算。

在这里插入图片描述

图7.14 Blinn-Phong计算

这些计算可以在片段着色器中进行,甚至为了性能考虑(经过一些调整)也可以在顶点着色器中进行。图7.15展示了使用Blinn-Phong 着色的环面。它在图形质量上几乎与Phong渲染相同,同时节省了大量性能损耗。
在这里插入图片描述

图7.15 Blinn-Phong着色的环面

程序7.3中展示了修改后顶点着色器和片段着色器,它们用来将程序7.2中的Phong着色示例转换为Blinn-Phong着色。C++ / OpenGL代码与之前一样没有变化。

程序7.3 Blinn-Phong着色的环面
main.cpp与程序7.1完全一致 唯一区别在于这里,由

renderingProgram = Utils::createShaderProgram("./GouraudShaders/vertShader.glsl", "./GouraudShaders/fragShader.glsl");

改成

renderingProgram = Utils::createShaderProgram("./BlinnPhongShaders/vertShader.glsl", "./BlinnPhongShaders/fragShader.glsl");

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;
out vec3 varyingHalfVector;// 角平分线向量 H 作为新增的输出

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)
{
    
    	
	//Blinn-Phong着色算法
	varyingVertPos = (mv_matrix * vec4(vertPos,1.0)).xyz;
	varyingLightDir = light.position - varyingVertPos;
	varyingNormal = (norm_matrix * vec4(vertNormal,1.0)).xyz;
	// 与之前的计算相同,增加了L+V的计算
	varyingHalfVector =
		normalize(normalize(varyingLightDir)
		+ normalize(-varyingVertPos)).xyz;

	gl_Position = proj_matrix * mv_matrix * vec4(vertPos,1.0);
}

fragShader.glsl

#version 430

in vec3 varyingNormal;
in vec3 varyingLightDir;
in vec3 varyingVertPos;
in vec3 varyingHalfVector;

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;
uniform mat4 mv_matrix;	 
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;

void main(void)
{
    
    	// 注意,现在已经不需要在片段着色器中计算R
	vec3 L = normalize(varyingLightDir);
	vec3 N = normalize(varyingNormal);
	vec3 V = normalize(-varyingVertPos);
	
	// 计算L与角平分线向量H之间的角度
	float cosTheta = dot(L,N);
	
	// 角平分线向量H已经在顶点着色器中计算过,并在光栅器中进行过插值
	vec3 H = normalize(varyingHalfVector);
	
	// 计算法向量N与角平分线向量H之间的角度
	float cosPhi = dot(H,N);

	// 计算逐像素的ADS分布
	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*3.0);// 最后乘以3.0作为改善镜面高光的微调
	fragColor = vec4((ambient + diffuse + specular), 1.0);
}

图7.16(见彩插)所示的两个例子展示了Phong着色应用在比较复杂的外部软件生成模型上所产生的效果。图7.16上图展示了Jay Turberville在Studio 522 Productions [TU16]创建的OBJ格式海豚模型的渲染图。图7.16下图是著名的“斯坦福龙”的渲染,斯坦福龙是1996年对一个小模型进行3D扫描所得到的模型[ST96]。两个模型都使用我们放在“Utils.cpp”文件中的“黄金”材质进行渲染。斯坦福龙因其大小而被广泛用于测试图形算法和硬件——它包含超过800 000个三角形。

在这里插入图片描述

图7.16 Phong着色的外部模型

7.6 结合光照与纹理

目前为止,在光照模型中,都是假设我们使用按ADS定义的光源,照亮按ADS定义材质的物体。但是,正如我们在第5章中所讲的,某些对象的表面可能会指定纹理图像。因此,我们需要一种方法来结合采样纹理所得的颜色和光照模型产生的颜色。

我们结合光照和纹理的方式取决于物体的特性以及其纹理的目的。这里有多种情况,其中常见的有:

  • 纹理图像很写实地反映了物体真实的表面外观;
  • 物体同时具有材质和纹理;
  • 材质包括了阴影和反射信息(在第8章、第9章中);
  • 有多种光和/或多个纹理。

我们先来观察第一种情景,物体拥有一个简单的纹理,同时我们对它进行光照。实现这种光照的一种简单方法是在片段着色器中完全将材质特性去除掉,之后使用纹理取样所得纹理颜色代替材质的ADS值。下面的伪代码展示了这种策略:

fragColor = textureColor * ( ambientLight + diffuseLight ) +specularLight

这种策略下,纹理颜色影响了环境光和漫反射分量,而镜面反射颜色仅由光源决定。镜面反射分量仅由光源决定是一种很常见的做法,尤其是对于金属或“闪亮”的表面。但是,对于不那么闪亮的表面,如织物或未上漆的木材(甚至一小部分金属,如黄金),其镜面高光部分都应当包含物体表明颜色。在这些情况下,之前的策略应该做适当微调:

fragColor = textureColor * ( ambientLight + diffuseLight + specularLight )

同时也有一些情况下,物体本身具有ADS材质,并伴有纹理图像。 如银质物体使用纹理为表面添加一些氧化痕迹。在这些情况下,如之前章节中所讲过的,既用到光照又用到材质的标准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

图7.17(见彩插)展示了拥有UV映射纹理图像(来自Jay Turberville[TU16])的Studio 522海豚,以及我们之前在第6章见过的 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。

在这里插入图片描述

图7.17 结合光照与纹理

补充说明

图7.7所展示的面片着色的环面是通过在顶点着色器和片段着色器中,将“flat”插值限定符添加到相应的法向量属性声明中得到的。 这样会使得光栅器不对所限定的变量进行插值,而是直接将相同的值赋给每个片段(在默认情况下,它会选择三角形第一个顶点上的值)。在Phong着色示例代码中,可以通过如下修改实现面片着色:

//在顶点着色器中
flat out vec3 varyingNormal;
//在片段着色器中
flat in vec3 varyingNormal;

我们还没有讨论的一类很重要的光是分布式光(distributed light)(或区域光(area light)),这种光的光源是一片区域而非一个单点。它在现实世界相对应的例子是通常在办公室或教室中的日光灯管。有兴趣的读者可以在[MH02]找到更多有关区域光的详细信息。

历史记录

在本章中我们过度简化了Gouraud和Phong的一些术语。Gouraud着色归功于Gouraud——通过计算顶点上光的强度并使用光栅器对光强进行插值以生成平滑的曲面外观(有时也被称为“平滑着色”)。Phong 着色则归功于Phong,这是另一种平滑着色,对法向量插值并计算每个像素的光照。Phong同时也被认为是成功将镜面高光纳入平滑着色的先驱者。因此,ADS光照模型在计算机图形学中也通常被称为Phong反射模型。因此,我们例子中的Gouraud着色准确地来说是使用了Phong反射模型的Gouraud着色。由于Phong的反射模型在3D图形编程中非常普及,通常Gouraud着色模型都是在Phong反射模型中进行展示。不过这可能会引起误会,因为原本Gouraud在1971年的工作中并没有任何镜面反射分量。

猜你喜欢

转载自blog.csdn.net/weixin_44848751/article/details/130930351