07Transformations

其中 y=x2y=x^2 表示向量 vˉ\bar{\color{red}v} 的长度。通过在方程中添加 z2z^2 ,可以很容易地将 x2x^2 其扩展到 3D。

I will give you \$2 if you can solve

y=x2y = x^2

(123)+x(123)+(xxx)=(1+x2+x3+x) \begin{pmatrix} \color{red}1 \\ \color{green}2 \\ \color{blue}3 \end{pmatrix} + x \rightarrow \begin{pmatrix} \color{red}1 \\ \color{green}2 \\ \color{blue}3 \end{pmatrix} + \begin{pmatrix} x \\ x \\ x \end{pmatrix} = \begin{pmatrix} \color{red}1+x \\ \color{green}2+x \\ \color{blue}3+x \end{pmatrix}

转变#

我们现在知道如何创建物体,给它们着色,或者用纹理赋予它们精细的外观,但它们仍然不够有趣,因为它们都是静态物体。我们可以尝试通过改变它们的顶点并在每一帧重新配置它们的缓冲区来让它们动起来,但这很麻烦,而且会消耗大量的处理能力。其实有更好的方法来变换物体,那就是使用(多个)矩阵对象。但这并不意味着我们要讨论功夫和庞大的数字人工世界。

矩阵是非常强大的数学结构,乍一看似乎令人生畏,但一旦你熟悉了它们,就会发现它们非常有用。在讨论矩阵时,我们需要稍微了解一些数学知识,对于数学基础较好的读者,我还会提供一些额外的阅读资源。

然而,要充分理解变换,我们首先需要深入研究向量,然后再讨论矩阵。本章的重点是为你提供一些我们后续会用到的基本数学知识。如果这些内容比较难懂,请尽量理解它们,并在需要时再回到本章复习相关概念。

向量#

从最基本的定义来看,向量就是方向,仅此而已。向量具有方向和大小(也称为强度或长度)。你可以把向量想象成藏宝图上的指示:“向左走10步,然后向北走3步,再向右走5步”;这里,“向左”是方向,“10步”是向量的大小。因此,藏宝图上的指示包含了3个向量。向量可以是任意维度,但我们通常处理的是2到4维的向量。二维向量表示平面上的一个方向(想想二维图形),三维向量可以表示三维空间中的任何方向。

下面你会看到三个向量,每个向量在二维图中用 (x,y) 表示为箭头。由于二维(而非三维)表示向量更直观,你可以将这些二维向量视为 z 坐标为 0 的三维向量。由于向量表示方向,向量的原点不会改变其值。在下面的图中我们可以看到,向量 v¯ 和 w¯ 即使原点不同,它们的值也相等:

06Textures

Textures#

我们了解到,为了增加物体的细节,我们可以为每个顶点设置颜色,从而创建一些有趣的图像。然而,为了获得相当逼真的效果,我们需要大量的顶点才能指定多种颜色。这会带来相当大的额外开销,因为每个模型都需要更多的顶点,并且每个顶点都需要一个颜色属性。

艺术家和程序员通常更喜欢使用纹理。纹理是一种二维图像(甚至还有一维和三维纹理),用于为物体添加细节;可以把纹理想象成一张印有精美砖块图案(例如)的纸,将其整齐地折叠覆盖在你的三维房屋上,使房屋看起来像是有石头外墙一样。因为我们可以在单个图像中添加大量细节,所以无需指定额外的顶点,就能使物体看起来极其精细。

除了图像之外,纹理还可以用来存储大量任意数据,以便发送到着色器,但我们将把这留到另一个话题中讨论。

下面你会看到一张 砖墙的纹理图像,它映射到上一章中的三角形上。

为了将纹理映射到三角形,我们需要告诉三角形的每个顶点它对应纹理的哪一部分。因此,每个顶点都应该关联一个纹理坐标,该坐标指定要从纹理图像的哪一部分进行采样。然后,片段插值会处理其他片段的剩余部分先画屏幕(三角形),然后再去纹理图片那里采样

纹理坐标在 x 轴和 y 轴方向上的取值范围均为 01 (请记住,我们使用的是二维纹理图像)。使用纹理坐标获取纹理颜色的过程称为采样。纹理坐标从纹理图像左下角的 (0,0) 开始,到纹理图像右上角的 (1,1) 结束。下图展示了如何将纹理坐标映射到三角形:

我们为三角形指定了 3 个纹理坐标点。我们希望三角形的左下角与纹理的左下角对应,因此我们使用纹理坐标 (0,0) 作为三角形左下角顶点的坐标。右下角也一样,使用纹理坐标 (1,0) 。三角形的顶部应与纹理图像的顶部中心对应,因此我们使用纹理坐标 (0.5,1.0) 。我们只需将这 3 个纹理坐标传递给顶点着色器,顶点着色器再将这些坐标传递给片段着色器,片段着色器会对每个片段的所有纹理坐标进行插值。

最终得到的纹理坐标如下所示:

float texCoords[] = {
    0.0f, 0.0f,  // lower-left corner  
    1.0f, 0.0f,  // lower-right corner
    0.5f, 1.0f   // top-center corner
};

纹理采样有多种不同的实现方式,其定义也比较宽泛。因此,我们的任务是告诉 OpenGL 应该如何对纹理 进行采样

Texture Wrapping#

纹理坐标通常介于 (0,0)(1,1) 之间,但如果我们指定超出此范围的坐标会发生什么?OpenGL 的默认行为是重复纹理图像(我们基本上忽略浮点纹理坐标的整数部分),但 OpenGL 还提供了更多选项:

  • GL_REPEAT :纹理的默认行为。重复纹理图像。 图片不断重复。坐标 1.1 看起来和 0.1 一样(忽略整数部分)
  • GL_MIRRORED_REPEAT :与 GL_REPEAT 相同,但每次重复都会镜像图像。 图片重复,但一次正、一次反。边缘处能完美衔接,没有裂缝感。
  • GL_CLAMP_TO_EDGE :将坐标限制在 01 之间。结果是,较高的坐标会被限制在边缘,从而形成拉伸的边缘图案。 坐标只要大于 1.0,就永远取 1.0 那一像素的颜色。结果是边缘被无限拉伸成直线。
  • GL_CLAMP_TO_BORDER :超出范围的坐标现在会赋予用户指定的边框颜色。 图片外面是一片纯色(你可以自己定颜色,比如黑色或透明)。

当使用超出默认范围的纹理坐标时,每个选项都会产生不同的视觉效果。让我们看看它们在示例纹理图像上的效果(原图由 Hólger Rezende 提供):

05Shaders

着色器#

正如 “你好,三角形” 一章中所述,着色器是运行在 GPU 上的小型程序。这些程序在图形管线的每个特定阶段运行。从本质上讲,着色器就是将输入转换为输出的程序。着色器也是高度隔离的程序,它们之间不允许通信;它们之间唯一的通信方式是通过输入和输出

上一章我们简要介绍了着色器及其正确使用方法。现在我们将更全面地解释着色器,特别是 OpenGL 着色语言 OpenGL Shading Language

GLSL#

着色器是用类似 C 语言的 GLSL 编写的。GLSL 专为图形处理而设计,包含专门针对向量和矩阵操作的实用功能。

着色器总是以版本声明开始后面跟着输入输出变量列表uniform 变量以及主函数。每个着色器的入口点都在其主函数中,该函数处理所有输入变量并将结果输出到输出变量中。如果您不了解 uniform 变量,请不要担心,我们稍后会讲解。

着色器通常具有以下结构:

#version version_number
in type in_variable_name;
in type in_variable_name;

out type out_variable_name;
  
uniform type uniform_name;
  
void main()
{
  // process input(s) and do some weird graphics stuff
  ...
  // output processed stuff to output variable
  out_variable_name = weird_stuff_we_processed;
}

当我们专门讨论顶点着色器时,每个输入变量也称为顶点属性。我们可以声明的顶点属性数量受硬件限制。OpenGL 保证始终至少有 16 个 4 分量顶点属性可用,但某些硬件可能允许更多,您可以通过查询 GL_MAX_VERTEX_ATTRIBS 来获取这些信息:

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

这通常会返回最小值 16 我的是32 ,这对于大多数用途来说应该绰绰有余。

03HelloWindow

Hello,Window#

我们来看看能不能让 GLFW 运行起来。首先,创建一个 .cpp 文件,并在新创建的文件顶部添加以下包含语句。

#include <glad/glad.h>
#include <GLFW/glfw3.h>

请务必在引入 GLFW 之前引入 GLAD。GLAD 的头文件包含了后台所需的 OpenGL 头文件(例如 GL/gl.h ),因此请确保在其他需要 OpenGL 的头文件(例如 GLFW)之前引入 GLAD。

接下来,我们创建主函数,并在其中实例化 GLFW 窗口:

int main()
{
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
  
    return 0;
}

在主函数中,我们首先使用 `glfwInit` 初始化 GLFW,之后可以使用 `glfwWindowHint` 配置 GLFW。`glfwWindowHint` 的第一个参数指定要配置的选项,我们可以从一个以 GLFW_ 为前缀的枚举列表中选择。第二个参数是一个整数,用于设置选项的值。所有可用选项及其对应值的列表可以在 GLFW 的窗口处理文档中找到。如果您现在尝试运行应用程序并出现大量_未定义引用_错误,则表示您没有成功链接 GLFW 库。

由于本书重点介绍 OpenGL 3.3 版本,我们需要告知 GLFW 我们要使用的 OpenGL 版本为 3.3。这样,GLFW 在创建 OpenGL 上下文时就能做出正确的配置。这确保了当用户没有安装正确的 OpenGL 版本时,GLFW 不会运行。我们将主版本号和次版本号都设置为 3 此外,我们还告知 GLFW 我们要==显式使用核心配置文件。使用核心配置文件意味着我们将获得 OpenGL 功能的一个较小子集,而不会使用我们不再需要的向后兼容功能。==请注意,在 macOS 系统上,您需要在初始化代码中添加 glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); 才能使其生效。

02创建窗口

在开始创建精美图形之前,我们需要做的第一件事是创建一个 OpenGL 上下文和一个用于绘制的应用程序窗口。然而,这些操作因操作系统而异,OpenGL 也刻意将这些操作抽象化。这意味着我们需要自行创建窗口、定义上下文并处理用户输入

幸运的是,有很多库可以提供我们所需的功能,其中一些是专门针对 OpenGL 的。这些库省去了我们所有与操作系统相关的繁琐工作,并为我们提供了一个窗口和一个 OpenGL 上下文来进行渲染。一些比较流行的库包括 GLUT、SDL、SFML 和 GLFW。在 LearnOpenGL 课程中,我们将使用 GLFW 。当然,您也可以使用其他任何库,大多数库的设置都与 GLFW 的设置类似。

GLFW#

GLFW 是一个用 C 语言编写的库,专门针对 OpenGL。GLFW 提供了在屏幕上渲染内容所需的基本功能。它允许我们创建 OpenGL 上下文、定义窗口参数并处理用户输入,这些功能足以满足我们的需求。

本章及下一章的重点是启动并运行 GLFW,确保它能正确创建 OpenGL 上下文,并显示一个简单的窗口供我们进行实验。本章将采用循序渐进的方式,讲解如何获取、构建和链接 GLFW 库。截至本文撰写之时,我们将使用 Microsoft Visual Studio 2019 IDE(请注意,在更新的 Visual Studio 版本中,此过程相同)。如果您未使用 Visual Studio(或更早的版本),也无需担心,在大多数其他 IDE 中,此过程也类似。

建造 GLFW#

您可以从 GLFW 的网页 下载页面获取它。GLFW 已经提供了适用于 Visual Studio 2012 到 2019 的预编译二进制文件和头文件,但为了完整起见,我们将从源代码自行编译 GLFW。这样做是为了让您体验自行编译开源库的过程,因为并非所有库都有预编译的二进制文件。那么,让我们下载_源代码包_吧。

我们将把所有库都构建成 64 位二进制文件,因此如果您使用的是预编译的二进制文件,请确保获取 64 位二进制文件。

下载完源代码包后,请解压缩并打开其内容。我们只需要其中的几个项目:

  • 编译后生成的库。
  • include文件夹。

从源代码编译库可以确保生成的库完美适配您的 CPU/操作系统,这是预编译二进制文件并不总是能提供的(有时,您的系统甚至没有可用的预编译二进制文件)。然而,向开源社区提供源代码的问题在于,并非所有人都使用相同的 IDE 或构建系统来开发应用程序,这意味着提供的项目/解决方案文件可能与其他人的配置不兼容。因此,其他人必须使用提供的 .c/.cpp 和 .h/.hpp 文件来配置自己的项目/解决方案,这非常繁琐。正是出于这些原因,才有了 CMake 这个工具

01OpenGL

在开始我们的探索之旅前,我们首先应该明确 OpenGL 究竟是什么。OpenGL 主要被视为一个 API(应用程序编程接口),它提供了一系列可用于操作图形和图像的函数。然而,OpenGL 本身并非 API,而仅仅是一个规范,由 Khronos Group 开发和维护。

OpenGL 规范明确规定了每个函数的输出结果及其执行方式。开发者需要根据规范 实现 这些函数,并找到相应的解决方案。由于 OpenGL 规范并未提供具体的实现细节,因此实际开发的 OpenGL 版本可以采用不同的实现方式,只要其结果符合规范(从而对用户而言相同)即可。

实际开发 OpenGL 库的是显卡制造商。你购买的每张显卡都支持特定版本的 OpenGL,这些版本是专门为该显卡(系列)开发的。在苹果系统中,OpenGL 库由苹果公司维护;而在 Linux 系统中,则存在着显卡供应商提供的版本以及爱好者对这些库的修改版本。这也意味着,如果 OpenGL 出现任何异常行为,很可能是显卡制造商(或开发/维护该库的人员)的问题。

由于大多数 OpenGL 实现都是由显卡制造商开发的,因此一旦实现中出现错误,通常可以通过更新显卡驱动程序来解决;这些驱动程序包含显卡支持的最新 OpenGL 版本。这也是为什么始终建议定期更新显卡驱动程序的原因之一。

Khronos 公开托管了所有 OpenGL 版本的规范文档。感兴趣的读者可以 在这里找到 OpenGL 3.3 版本的规范(我们将使用这个版本),如果您想深入了解 OpenGL 的细节,这份规范值得一读(请注意,其中大部分内容仅描述结果,而非具体实现)。这些规范也为理解其函数的具体工作原理提供了极佳的参考。

核心模式与即时模式#

过去,使用 OpenGL 意味着在即时模式(通常称为固定功能管线)下进行开发,这是一种易于使用的图形绘制方法。OpenGL 的大部分功能都隐藏在库内部,开发者对 OpenGL 的计算方式几乎没有控制权。随着开发者对灵活性的需求不断增长,规范也随之变得更加灵活,开发者获得了对图形的更多控制权。即时模式虽然易于使用和理解,但效率极低。因此,从 3.2 版本开始,规范逐渐弃用即时模式的功能,并鼓励开发者使用 OpenGL 的核心配置文件模式进行开发。核心配置文件模式是 OpenGL 规范的一个分支,它移除了所有旧的、已弃用的功能。

使用 OpenGL 的核心配置文件时,OpenGL 会强制我们使用现代编程实践。每当我们尝试使用 OpenGL 已弃用的函数时,OpenGL 都会抛出错误并停止绘制。学习现代方法的优势在于其灵活性和高效性。然而,它也更难学习。立即模式抽象了 OpenGL 执行的许多 实际操作,虽然易于学习,但很难理解 OpenGL 的实际运行机制。现代方法要求开发者真正理解 OpenGL 和图形编程,虽然这有点难度,但它提供了更大的灵活性、更高的效率,最重要的是:更深入地理解图形编程。

这也是本书以核心配置文件 OpenGL 3.3 版本为目标的原因。虽然难度更高,但绝对值得付出努力。

00介绍

欢迎来到这本在线 OpenGL 学习书籍!无论您学习 OpenGL 的目的是学术研究、职业发展还是纯粹出于兴趣爱好,本书都将使用 现代 (核心配置文件)OpenGL,为您讲解 OpenGL 的基础知识、中级知识以及所有高级知识。本书旨在以清晰易懂的方式和示例,向您展示现代 OpenGL 的方方面面,同时为后续学习提供有用的参考资料。

那么,为什么要读这些章节呢?#

互联网上有成千上万篇关于学习 OpenGL 的文档、书籍和资源,然而,其中大多数资源只关注 OpenGL 的即时模式(通常被称为旧版 OpenGL),内容不完整,缺乏完善的文档,或者不符合你的学习偏好。因此,我的目标是提供一个既完整又易于理解的平台。

如果您喜欢阅读提供循序渐进的指导、清晰示例且不会让您被海量细节淹没的内容,那么这本书可能正合您意。本书各章节旨在让没有任何图形编程经验的读者也能理解,同时也能为经验丰富的用户带来阅读乐趣。我们还会探讨一些实用的概念,只需稍加创意,就能将您的想法转化为真正的 3D 应用。如果您觉得以上描述与您自身情况相符,那么请继续阅读。

你将学到什么?#

这些章节的重点是现代 OpenGL学习(和使用)现代 OpenGL 需要扎实的图形编程知识,以及对 OpenGL 底层工作原理的了解,才能真正获得最佳体验。因此,我们将首先讨论核心图形方面的内容,即 OpenGL 如何将像素绘制到屏幕上,以及如何利用这些知识来创建一些炫酷的效果

除了核心知识之外,我们还将探讨许多可用于应用程序的实用技巧,例如:场景遍历、创建精美光照、从建模程序加载自定义对象、进行炫酷的后期处理等等。我们还会推出一系列演示视频,实际创建一个基于所学 OpenGL 知识的小游戏,让您真正体验图形编程的乐趣。

从哪里开始呢?#

学习 OpenGL 完全免费,而且永远免费,任何想要入门图形编程的人都可以使用。所有内容都可以在左侧菜单中找到。只需点击“简介”按钮,即可开始您的学习之旅!