OpenGL-学习指南-全-

OpenGL 学习指南(全)

原文:zh.annas-archive.org/md5/b815d84300bb8a0c0873a4be055c971e

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

OpenGL 是世界上最受欢迎的图形库;大多数移动游戏都使用 OpenGL,许多其他应用程序也是如此。在这本书中,你将了解构成我们玩的游戏和它们背后的游戏引擎的基础知识。通过逐步过程展示从设置 OpenGL 到其基本现代功能的一切。你将深入了解以下概念:使用 GLFW、SDL 和 SFML 在 Windows 和 Mac 上设置,2D 绘图,3D 绘图,纹理,光照,3D 渲染,着色器/GLSL,模型加载,和立方体贴图。

本书面向的对象

《学习 OpenGL》适合任何对创建游戏、了解游戏引擎工作原理感兴趣的人,最重要的是,对于任何对学习 OpenGL 感兴趣的人。这本书的理想读者是那些对学习游戏开发充满热情或寻找 OpenGL 参考指南的人。你在本书中学到的技能将适用于你所有的游戏开发需求。你需要有扎实的 C++基础来理解和应用本书中的概念。

本书涵盖的内容

第一章,设置 OpenGL,在这一章中,你将学习如何使用各种库设置 OpenGL:GLFW、GLEW、SDL 和 SFML。我们将学习如何在 Windows 和 Mac 上设置我们的 OpenGL 项目。我们还讨论了如何使用绝对或相对链接将库链接到你的项目中,并最终创建渲染窗口来显示 OpenGL 图形。

第二章,绘制形状和应用纹理,将引导你通过着色器绘制各种形状。我们将从绘制一个三角形开始,并学习如何为其添加颜色。然后,我们将使用三角形的概念来绘制我们的四边形,并学习如何为其添加纹理。

第三章,变换、投影和摄像机,这一章在上一章的基础上进一步展开。你将学会如何将旋转和变换等变换应用到我们的形状上,并学习如何绘制一个立方体并为其添加纹理。然后,我们将探讨投影(透视和正交)的概念,并在我们的游戏世界中实现这些概念。

第四章,光照、材质和光照贴图的效果,在这一章中,我们将学习如何为我们的对象添加颜色,以及如何在游戏世界中创建光源,例如灯。然后,我们将研究光照对对象的影响。你将了解不同的光照技术:环境光、漫反射、镜面反射。我们还将探索各种真实世界的材质,并观察光照对材质的影响。你还将在本章中学习关于光照贴图的内容。

第五章, 光源类型和灯光组合, 本章将讨论不同类型的光源,如方向光、点光源和聚光灯。我们还将学习如何组合游戏世界中的灯光效果和光源。

第六章, 使用立方体贴图实现天空盒, 在本章中,您将使用立方体贴图生成天空盒。您将学习如何将纹理应用到天空盒上,并创建一个单独的纹理文件,以便在代码中更容易地加载纹理。您还将学习如何绘制天空盒,并使用它来创建我们的游戏世界。

在线章节, 模型加载, 这是可在线获取的额外章节,地址为www.packtpub.com/sites/default/files/downloads/ModelLoading.pdf。在本章中,您将学习如何使用 CMake 在 Windows 上设置 Assimp(Open Asset Import Library),以满足我们所有模型加载的需求。我们还将介绍如何在 Mac OS X 上设置 Assimp,并创建一个跨平台网格类。然后我们将探讨如何将 3D 模型加载到我们的游戏中。您还将学习如何创建一个模型类来处理我们模型的加载。

要充分利用本书

对于本书,您对 C++有良好的基础非常重要,因为您将在此书中使用 OpenGL 与 C++结合。OpenGL 并不容易,如果您是第一次编码或编码时间不长,建议您先掌握 C++,然后再继续阅读本书。

免责声明

本书使用的插图仅用于说明目的。我们不推荐您以任何方式滥用这些插图。有关更多信息,请参阅此处提到的出版商的条款和条件。

任天堂 : www.nintendo.com/terms-of-use

下载示例代码文件

您可以从www.packt.com的账户下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packt.com登录或注册。

  2. 选择支持选项卡。

  3. 点击代码下载与勘误。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

文件下载后,请确保您使用最新版本的以下软件解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Learn-OpenGL。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富的书籍和视频目录的代码包,可在github.com/PacktPublishing/找到。去看看吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/LearnOpenGL_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“在外部库文件夹中提取 GLEW 和 GLFW 的库文件。”

代码块设置如下:

   SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); 
   SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); 
   SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); 

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

   SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); 
   SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); 
   SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); 

任何命令行输入或输出都按以下方式编写:

brew install glfw3 
brew install glew

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“打开 Xcode 并点击创建新 Xcode 项目选项。”

警告或重要注意事项如下所示。

技巧和窍门如下所示。

联系我们

我们欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com给我们发邮件。

勘误表:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问www.packt.com/submit-errata,选择您的书,点击勘误表提交表单链接,并输入详细信息。

盗版:如果您在互联网上遇到我们作品的任何形式的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问authors.packtpub.com

评论

请留下您的评价。一旦您阅读并使用了这本书,为何不在购买它的网站上留下评价呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 公司可以了解您对我们产品的看法,并且我们的作者可以查看他们对书籍的反馈。谢谢!

如需了解更多关于 Packt 的信息,请访问packt.com

第一章:设置 OpenGL

欢迎来到现代 OpenGL 的世界。开放图形库OpenGL)是一个 API,它为开发者提供了一系列函数,使他们能够操纵图形和图像。它是当今大多数游戏的核心框架,无论是 iOS 或 Android 的移动游戏,还是其他平台,如桌面和游戏机。OpenGL 本身就是最好的证明。看看你能想到的任何类型的游戏,在 OpenGL 中都是可以实现的。它不仅限于 3D;你还可以创建 2D 游戏。2D 和 3D 游戏引擎都是使用 OpenGL 创建的,所以它能够胜任你所能想到的一切。在本书中,我们将学习所有必要的 3D 游戏开发概念。

在本章中,我们将探讨如何在 Windows 和 Mac 上使用各种库(如 GLFW、GLEW、SDL 和 SFML)设置 OpenGL。本章的主要重点是了解如何下载这些库以及如何使用它们设置 OpenGL。在我们学习如何设置项目的同时,我们还将探讨如何使用绝对和相对链接将这些库链接到我们的项目中。

本章节将涵盖以下主题:

  • 下载必需的库

  • 在不同平台上设置项目以使用库

  • 使用库创建 OpenGL 渲染窗口

您可以在 GitHub 上的Chapter01文件夹中找到本章节的所有代码文件。GitHub 链接可以在本书的序言中找到。

在 Windows 上使用 GLFW 和 GLEW 设置 OpenGL

在本节中,我们将学习如何在 Windows 系统上使用 Visual Studio 通过 GLFW 和 GLEW 设置 OpenGL,但首先让我们了解 GLFW 和 GLEW 是什么。GLFW是一个OpenGL 框架。这是一个非常轻量级的框架,它允许我们检测事件,如键盘输入、鼠标输入和其他类型的输入,但更重要的是,它允许你创建一个渲染窗口,你可以在其中渲染你的代码,因为 OpenGL 没有创建渲染窗口的方法,它需要像 GLFW 这样的东西来创建。

GLEWOpenGL 扩展包装器,它基本上允许你使用新的 OpenGL 函数,或者更确切地说,非核心函数。它在运行时提供系统机制来确定目标平台支持哪些 OpenGL 扩展。对于任何新函数,你本质上需要像 OpenGL 扩展包装器这样的东西来初始化扩展并编写可移植的应用程序。

首要之事:对于本书来说,你有一个良好的 C++基础非常重要,因为在本书中,你将使用 C++与 OpenGL 结合。OpenGL 并不容易。如果你是第一次编码或者编码时间不长,建议你先掌握 C++,然后再继续阅读本书。

让我们通过下载 GLFW 和 GLEW 库来开始我们的设置过程。

下载必需的库

让我们开始设置,按照以下步骤操作:

  1. 首先,我们需要 Visual Studio。您可能已经安装了它。如果您已经安装,那真是太好了。如果没有,请访问visualstudio.microsoft.com/,转到“下载”,然后点击下载Visual Studio Community 2017版本。然后,按照说明将 Visual Studio 安装到您的系统上。一旦安装完成,您只需确保它已设置为 C++环境。

如果您有专业版本,那真是太好了,但社区版本也完全足够。

  1. 接下来,我们将下载OpenGL 扩展管理器库。访问glew.sourceforge.net/,然后点击“二进制”选项下载 32 位或 64 位版本文件,根据您的系统需求:

图片

下载 GLEW 的二进制文件

下载完成后,只需解压文件并将其放置在您认为方便访问的地方,因为对于这个项目以及您创建的任何其他项目,它将引用该目录。所以,您不想移动它,因为那样您将不得不重新设置您项目的设置。对于这个项目,建议您在C:驱动器中创建一个名为OpenGL的文件夹,并将所有下载的库放在里面。这将帮助您在链接库到项目时更容易访问。

当您解压文件时,它们不会被很好地命名,您可能会发现所有版本号和相关信息都让人困惑。因此,为了去除任何版本信息文本,最好将文件夹重命名为像GLEWGLFW这样简单的东西。这样很整洁,您可以轻松知道自己在做什么,并且这使得查看事物变得容易得多。

  1. 一旦完成,我们将继续下载OpenGL 框架库文件。访问www.glfw.org/并点击“下载”菜单。我们将需要下载 Windows 的预编译二进制文件。如以下截图所示,根据您的系统需求选择并点击下载 32 位或 64 位版本:

图片

下载 GLFW 的二进制文件

注意:即使您知道您需要下载 64 位版本来在 64 位机器上进行开发,也尽量坚持使用 32 位版本,因为除非您认为您的游戏或应用程序将使用超过 4GB 的内存,否则 32 位版本将完全足够,并且有助于最大化兼容性。

下载文件后,解压它,如前所述,将其放置在OpenGL文件夹内的GLFW文件夹中。

使用绝对链接链接 GLFW 和 GLEW 库

在我们下载了所有必要的文件后,我们将使用 GLFW 和 GLEW 库在 Visual Studio 中为 OpenGL 设置环境。按照以下步骤操作:

  1. 打开 Visual Studio,然后点击创建新项目...:

截图

Visual Studio 启动页面

  1. 然后,转到 Visual C++ | Windows 桌面 | Windows 控制台应用程序,并将你的项目命名为 GLFWOpenGL,如以下截图所示,然后点击 OK:

截图

创建新项目

如果你没有在新建项目窗口中看到 Visual C++ 选项,你可能需要下载 Visual C++。更多信息,你可以访问以下链接:

docs.microsoft.com/en-us/cpp/build/vscpp-step-0-installation

  1. 现在,在解决方案资源管理器窗口中右键单击项目。转到添加 | 新项,你将得到一个添加新项窗口。选择 C++ 文件,因为这将是我们的主文件,让我们将其命名为 main.cpp,然后点击添加按钮。

  2. 接下来,在解决方案资源管理器窗口中右键单击项目 点击属性

  3. 将弹出一个属性页窗口;点击 C/C++ | 通用,然后转到附加包含目录。点击下拉菜单,然后点击 <编辑>,你将得到如下弹窗:

截图

添加包含目录

  1. 如前一张截图所示,点击新建按钮,然后点击三个点。现在,浏览到 OpenGL 文件夹中的 GLEW 文件夹。选择 include 文件夹,然后点击选择文件夹按钮。接下来,我们重复相同的步骤将 GLFW 库添加到我们的项目中。一旦我们包含了这两个库,点击 OK 按钮。

  2. 现在,再次在属性页窗口中,我们将转到链接器 | 通用,然后转到附加库目录。点击下拉菜单,然后点击 <编辑>,你将得到一个弹窗,如下所示:

截图

添加库

  1. 如前一张截图所示,点击新建按钮,然后点击三个点。现在,浏览到你下载 GLEW 文件的 OpenGL 文件夹。打开 GLEW 文件夹中的 lib 文件夹,然后双击 Release 文件夹,选择 Win32,然后点击选择文件夹按钮。

  2. 重复相同的步骤以包含 GLFW 库。但对于 GLFW,你有一系列不同的库可以选择。对于我们的项目,最好选择 lib-vc2015 文件夹。一旦你添加了这两个库,点击 OK 按钮。

有许多其他版本的库可供选择用于 GLFW。所以,如果你有较旧的 Visual Studio 版本,你可以选择那个特定版本的库。

  1. 接下来,我们将转到“链接器 | 输入”,然后转到“附加依赖项”。点击下拉菜单,然后点击“编辑”。在这里,我们将输入opengl32.lib到文本框中,如以下截图所示突出显示。opengl32.lib是内置在操作系统中的库。接下来,我们将输入glew32s.lib。这是一个静态库,它将被静态链接到你的项目中。如果你不想静态链接它,你可以简单地从后缀中移除s;这取决于你。接下来,我们将输入glfw3.lib,然后点击“确定”按钮:

图片

添加额外的依赖项

  1. 然后,点击“应用”按钮。

在上一节中,我们讨论了如何下载必需的库以及如何使用绝对链接将它们链接到我们的项目中。

在下一节中,我们将研究如何使用相对链接将这些库链接到我们的项目中,我们还将了解相对链接对我们有何益处。你可以选择其中任何一个来将库链接到你的项目中;这是你的选择。

使用相对链接链接 GLFW 和 GLEW 库

在本节中,我们将探讨如何使用 GLFW 作为提供者设置 OpenGL,以相对链接创建渲染窗口。在上一节中,我们讨论了绝对链接,所以,让我们快速概述一下绝对链接和相对链接实际上是什么。

绝对链接是一个将库明确链接到项目的过程。例如,如果你创建了一个项目,并且正在链接像 GLFW 和 GLEW 这样的库,在链接它们时,你将明确输入它们所在目录的路径。如果它们在C:驱动器上,你实际上会输入显式的目录。但是,如果你将库文件移动到任何其他位置,那么你就必须更新你的 Visual Studio 项目以反映更改的路径。

使用相对链接,库实际上被链接,但相对于项目。所以,你不会说库在C:驱动器上;相反,你说它们从特定文件夹相对链接到你的项目。所以即使你移动了库,也不会影响你的项目。这是一个在需要良好视觉编辑器的平台上传输项目的绝佳方法。当你在一个实际上没有良好视觉编辑器的平台上工作时,这种方法更适合开发;例如,Unity 或 Unreal 这样的平台。

因此,让我们开始相对链接我们的库并创建一个 OpenGL 渲染窗口。让我们打开 Visual Studio 并按照以下步骤操作:

  1. 点击“创建新项目...”并转到 Visual C++ | Windows 桌面 | Windows 控制台应用程序。将项目命名为GLApp(因为我们正在学习如何相对链接库,所以我们创建了一个不同的项目)。

  2. 然后,在新项目窗口中,点击浏览...按钮。转到桌面上的 OpenGL 文件夹(我们使用这种文件夹结构格式来理解相对链接)。只需选择文件夹,然后点击确定。

  3. 在开始项目之前,你需要做的一件事是在桌面上的 OpenGL 文件夹内创建一个名为 External Libraries 的文件夹。在 External Libraries 文件夹中提取 GLEW 和 GLFW 的库文件。

  4. 现在,我们将右键单击解决方案资源管理器窗口中的项目。转到添加 | 新项。选择 C++ 文件,并将其命名为 main.cpp,然后点击添加按钮。

  5. 接下来,在解决方案资源管理器窗口中右键单击项目,转到属性

  6. 将会弹出一个属性页面窗口;点击 C/C++ | 通用,然后转到附加包含目录。在其中,点击下拉菜单,然后点击 <编辑>:

图片

添加包含目录

  1. 然后,点击新按钮。由于我们在本节中进行相对链接,所以不会点击三个点。点击它们仅用于绝对链接,因为我们必须浏览到存储库的目录。

  2. 在前一个截图突出显示的文本框中,键入 $(SolutionDir);这个命令指的是包含我们的 .sln 文件的文件夹。所以如果我们指定路径中的文件夹,每次我们在项目中做新的操作时,它都会相对链接到该文件所在的位置。

  3. 要将文件包含到我们的项目中,添加以下截图所示的路径,然后点击确定按钮:

图片

  1. 接下来,我们将链接库。所以,在属性页面窗口中,我们将转到链接器 | 通用,然后转到附加库目录。点击下拉菜单,点击编辑,然后点击新建。添加以下截图所示的路径,然后点击确定,然后应用:

图片

  1. 现在,我们还有一件事要做,那就是链接 .lib 文件。所以,转到链接器 | 输入,然后转到附加依赖项。点击下拉菜单,然后点击 <编辑>。现在,在文本框中,只需键入 opengl32.lib。这个库文件不是与 GLFWGLEW 一起下载的;它是内置在 Windows 中的。接下来,在新的一行中,只需键入 glew32s.lib,然后对于 GLFW lib-vc2015,键入 glfw3.lib。然后,点击确定并点击应用按钮。

无论你选择哪种链接过程,你都可以按照那个方法进行。使用你用来链接库的任何一种方法,我们都需要完成一个最后的步骤,才能开始编码,那就是将动态链接库复制并粘贴到我们的项目中。

将动态链接库添加到项目中

让我们看看这些步骤,并了解如何将一个 动态链接库 (dll) 添加到我们的项目中:

  1. 前往 C: 驱动器上的 OpenGL 文件夹;在其中,进入 GLEW 文件夹,打开它并进入 bin,双击它,然后进入 Win32 并打开它。然后,复制如以下截图所示的高亮显示的 glew32.dll 动态链接库:

图片

glew32.dll 动态链接库

  1. 按照上一步的说明,将 GLFW 的 .dll 文件添加到你的项目中

  2. 现在,前往你的系统中 main.cpp 文件所在的位置,并将复制的动态链接库文件粘贴在那里。

通过这一最后步骤,我们已经完成了 OpenGL 的设置,并且已经将库绝对或相对地链接到我们的项目中。我们现在可以开始编写 OpenGL 渲染窗口的代码了。

在前面的部分,我们讨论了如何在 Windows 平台上设置 OpenGL。但是,如果你在 Mac 系统上工作怎么办?因此,让我们看看我们如何在 Mac 平台上下载库并设置 OpenGL。

在 Mac 上使用 GLFW 设置 OpenGL

到目前为止,我们已经讨论了如何设置我们的项目以在 Windows 上使用 GLFW 库。在本节中,我们将讨论如何在 Mac 系统上设置 OpenGL。那么,让我们开始吧。

在 Mac 上下载 GLFW 和 GLEW 库

要将必需的库下载并安装到你的 Mac 系统中,我们必须安装一个名为 Homebrew 的 Mac 包管理器。Homebrew 将帮助我们安装所有必要的包和库来运行我们的 OpenGL 代码。

要安装 Homebrew,请访问 brew.sh/,复制以下截图中的高亮显示的路径,将其粘贴到你的终端中,然后按下 Enter。提示将会在你的系统中下载并安装 Homebrew:

图片

Homebrew 主页上的路径

一旦我们安装了 Homebrew,我们就会将 GLFW 和 GLEW 库下载到我们的系统中。让我们首先安装 GLFW。为此,我们需要在终端窗口中输入以下命令:

brew install glfw3 

在前面的命令中,你必须已经注意到我们包含了数字 3;这样做的原因是,如果你只输入 glfw,它会安装一个较旧的版本,而我们不希望这样做,所以插入 glfw3 将会安装最新版本。按下 Enter 键,库将会被下载到你的系统中。

现在,我们将对 GLEW 执行相同的操作;在终端中输入以下命令:

brew install glew  

我们不需要为这个版本做任何设置;只需按下 Enter 键,必要的文件就会被下载。这就是将库下载到我们系统中的全部过程。

注意,由于我们是在系统本身上安装库,而不是在我们的项目中安装,所以每次你将项目移动到不同的系统时,你都需要在那个特定的系统上安装这些库。

一旦我们借助 Homebrew 下载并安装了所有必需的库,我们接下来将进行设置 Xcode 以使用 OpenGL。

确保 Xcode 已安装到您的系统上。如果没有,请按照以下说明在您的系统上安装它。

设置 Xcode 以使用 OpenGL

在本节中,我们将讨论如何设置 Xcode 以运行我们的 OpenGL 代码。按照以下步骤执行设置过程:

  1. 打开 Xcode,并点击创建一个新的 Xcode 项目选项。

  2. 前往 OS X | 应用程序,选择命令行工具,然后点击下一步。

  3. 您将看到以下窗口;填写必要的详细信息,如以下屏幕截图所示:

图片

项目的基本细节

  1. 在前面的屏幕截图中,请确保语言选项始终设置为C++,然后点击下一步。组织名称和组织标识符属性,您可以设置为任何您想要的。

  2. 接下来,设置您希望存储和保存项目的位置。然后,点击创建按钮。接下来,我们有一个普通的 C++项目准备好了。在我们开始编写代码之前,我们需要遵循一些额外的步骤来设置我们的项目。

  3. 首先,在 Xcode 中,点击您的项目并转到构建设置。在构建设置中,转到搜索路径部分,并点击头文件搜索路径。然后,点击加号并输入/usr/local/include。这将允许我们在main.cpp文件中#include GLEW 和 GLFW。

  4. 现在转到构建阶段,然后点击链接二进制与库,并点击加号按钮。在搜索栏中输入opengl,选择 OpenGL.framework,然后点击添加按钮。

  5. 再次点击加号按钮,然后点击添加其他....现在,按Cmd + Shift + G,它将打开一个go-to文件夹搜索栏。在它里面,输入/usr/local。然后点击 Cellar,转到 glew | lib 文件夹,选择没有小箭头的libGLEW.1.12.0.dylib,然后点击打开。

箭头只是一个快捷方式,一个别名,我们不希望这样。我们也不希望 MX 版本,只希望普通的.dy非别名库。

  1. 再次点击加号,然后点击添加其他...,按Cmd + Shift + G,并输入/usr/local。现在转到 Cellar,转到 glfw | lib。选择非别名的libglfw3.3.1.dylib并点击打开。

执行所有步骤后,我们的项目现在已设置好,可以在 Mac 上使用 GLEW 和 GLFW 与 OpenGL。我们现在可以转到 Xcode 中的main.cpp文件,并开始编写创建 OpenGL 渲染窗口的代码。

使用 GLFW 创建 OpenGL 渲染窗口

让我们去 Visual Studio 或 Xcode 中的main.cpp文件,然后开始。在您的编辑器中开始输入以下代码:

  1. 首先,向我们的代码中添加一些头文件:
#include <iostream> 

// GLEW 
#define GLEW_STATIC 
#include <GL/glew.h> 

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

iostream只是 C++中内置的输入/输出流。然后,使用GLEW_STATIC,我们静态链接了 GLEW。如果您不想静态链接它,只需省略#define行。

  1. 接下来,我们将创建一些常量,这些常量将用于存储窗口的宽度和高度:
// Window dimensions 
const GLint WIDTH = 800, HEIGHT = 600; 

你可能会想,为什么我们使用 GLint 而不是普通的 int?原因在于不同操作系统上普通 int 的问题;例如,在不同的编译器中,它可能有不同的长度。使用 GLint,它在任何编译器上都是一致的,因此这是一种确保最大兼容性的好方法。

  1. 现在,我们将使用 int main 设置我们的主入口点,然后初始化 GLFW:
// The MAIN function, from here we start the application and run the game loop 
int main() 
{ 
   // Init GLFW 
   glfwInit(); 
  1. 接下来,我们将设置一些窗口提示,这实际上是我们将为窗口设置的某些属性:
// Set all the required options for GLFW 
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); 
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); 

我们选择 3.3 的原因是因为在 3.1 版本之后,旧版本的 OpenGL 中的代码已被弃用。这样做是为了禁止开发者使用旧版本的 OpenGL。从 3.3 版本开始,OpenGL 版本与着色器版本相匹配。因此,对于 3.3 版本,OpenGL 着色器语言版本也是 3.3;这有助于保持事物的一致性、整洁和有序。但如果你需要新功能,请随意使用类似 4.3 的版本。

  1. 接下来,我们将输入一些更多的窗口提示:
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); 

在这个项目中,我们将使用 CORE_PROFILE。实际上,有两种主要的配置文件可供选择:核心配置文件和兼容性配置文件,COMPAT_PROFILE。我们在项目中使用 CORE_PROFILE 的原因是 CORE_PROFILE 使用新的 OpenGL 功能,而兼容性配置文件使用旧的方法做事,从而确保最大的兼容性。你可能可能会想,即使它确保了最大的兼容性,为什么建议不要使用 COMPAT_PROFILE?原因在于这本书中你正在学习 OpenGL 的一般知识,所以我们不想学习过时的方法。相反,我们想学习使用顶点对象和顶点数组来存储图形卡上的新、现代 OpenGL。因此,如果你使用兼容模式,你只是在使用诸如 glBegin 之类的功能时陷入不良实践。所以,这就是我们将其设置为核心配置文件的原因。

  1. 一旦我们设置了配置文件,我们将设置窗口提示以获取向前兼容性:
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); 

这个窗口提示实际上在 macOS 上是必需的,否则它将崩溃,但在 Windows 上也没有任何害处。

  1. WindowHint 中,我们将设置 GLFW_RESIZABLE,并将其设置为 FALSE 以防止窗口被调整大小。如果您希望它可以调整大小,只需将其设置为 TRUE
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); 
  1. 接下来,我们将创建我们的窗口。为此,我们将添加以下代码:
// Create a GLFWwindow object that we can use for GLFW's functions 
GLFWwindow *window = glfwCreateWindow(WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr); 

在前面的代码中,我们调用变量 WIDTHHEIGHT 的值。这些术语定义了窗口的大小,而 "LearnOpenGL" 设置了窗口的标题。窗口和监视器变量被定义为空指针,我们将在后面的章节中处理这些问题。

  1. 接下来,我们将定义屏幕宽度和高度的变量,因为这将是我们希望窗口设置的实际分辨率:
int screenWidth, screenHeight; 

然后,在接下来的代码行中,使用 glfwGetFramebufferSize,我们传递屏幕宽度和高度的引用:

glfwGetFramebufferSize( window, &screenWidth, &screenHeight ); 

这行代码实际上获取的是屏幕窗口本身的实际宽度,相对于屏幕的密度。当你创建 OpenGL 视口时,你可以有效地省略这些行,只使用 screenWidthscreenHeight 的值。但是,如果你有像 Mac 或 Retina Mac 这样的设备,它们不是原生 1920 x 1080,或者例如具有 3840 x 2160 分辨率的更高密度屏幕,窗口就会变得混乱。内容将显示在屏幕的左下角或右上角。上一行代码帮助我们获取窗口的实际宽度和高度,相对于任何像素密度的变化。因此,建议保留它,因为它将确保未来的最大兼容性,因为越来越多的超高分辨率屏幕正在出现。

  1. 现在,我们想要检查窗口是否成功创建,我们将按照以下步骤进行:
if (nullptr == window) 
{ 
       std::cout << "Failed to create GLFW window" << std::endl; 
       glfwTerminate(); 

       return EXIT_FAILURE; 
} 

在前面的代码中,我们检查了 nullptr == window 条件,并让用户知道出了些问题。然后,我们只是使用 glfwTerminate(); 终止任何已初始化的内容,并最终退出:

glfwMakeContextCurrent(window); 
  1. 接下来,我们需要启用 GLEW,我们将按照以下步骤进行:
// Set this to true so GLEW knows to use a modern approach to retrieving function pointers and extensions 
   glewExperimental = GL_TRUE; 

看到代码中的 glewExperimental,你可能会想知道我们是否在使用实验性功能?为什么我们要将其设置为 TRUE?这样做的原因是 GLEW 知道使用现代方法来检索函数、指针和扩展。基本上,这只是说我们正在使用 GLEW 的新和现代方式,但这并不一定是一个实验性功能。

  1. 然后,我们将初始化 GLEW 并确保一次性成功初始化:
// Initialize GLEW to setup the OpenGL Function pointers 
if (GLEW_OK != glewInit()) 
{ 
      std::cout << "Failed to initialize GLEW" << std::endl; 
      return EXIT_FAILURE; 
} 

你也可以使用 return -1 而不是 return EXIT_FAILURE; 来代替 Xcode。

  1. 接下来,我们将设置 OpenGL 视口:
   // Define the viewport dimensions 
   glViewport(0, 0, screenWidth, screenHeight); 

在上一行代码中,我们所做的是将初始坐标从 0, 0 设置到 screenWidthscreenHeight。在这里获取的值将是我们窗口相对于屏幕的准确表示,因为你的屏幕可能有更高的或更低的像素密度。

  1. 因此,现在我们已经设置了视口,我们将创建我们的游戏循环:
// Game loop 
while (!glfwWindowShouldClose(window)) 
{ 
       // Check if any events have been activiated (key pressed, 
       //mouse moved etc.) and call corresponding response functions 
       glfwPollEvents(); 

       // Render 
       // Clear the colorbuffer 
       glClearColor(0.2f, 0.3f, 0.3f, 1.0f); 
       glClear(GL_COLOR_BUFFER_BIT); 

      // Draw OpenGL 

      glfwSwapBuffers(window); 
} 

在前面的代码中,我们创建了一个 While 循环并将其初始化为检查窗口是否打开;如果是,则运行循环。在循环中,我们使用 glClearColor 函数清除 colorbufferClearColor 实际上是一行可选代码,但我们添加它的原因是如果我们不添加它,我们可能会得到一个空白、黑色的背景,因为我们还没有绘制任何东西。所以,我们试图用一些颜色来美化它。我们定义了介于 0 和 1 之间的颜色,这与介于 0 和 255 之间的范围相当相似,其中 0 没有价值,1 是红色、绿色、蓝色和 alpha 的完全强度。

  1. 然后,我们添加了 glClear 来清除我们的窗口,以便我们准备好绘制下一帧,并在其中放入 GL_COLOR_BUFFER_BIT;。这就是你将绘制 OpenGL 内容的地方。由于我们本章不会绘制任何内容,我们将添加 glfwSwapBuffers 并将其提供给窗口。然后,在 while 循环执行后,我们将添加 glfwTerminate 来关闭窗口:
// Terminate GLFW, clearing any resources allocated by GLFW. 
glfwTerminate(); 

return EXIT_SUCCESS; 
}    

你也可以使用 return -1 代替 return EXIT_FAILURE; 来用于 Xcode。

现在,让我们运行此代码并检查输出。你将在屏幕上看到一个类似的 OpenGL 窗口:

图片

Windows 的 OpenGL 渲染窗口

在 Windows 上使用 SDL 设置 OpenGL

在本节中,我们将讨论如何在 Windows 机器上使用 SDL 和 GLEW 设置 OpenGL。SDL 代表 Simple DirectMedia Layer,它允许我们创建渲染窗口,并通过 OpenGL 提供对输入设备的访问。SDL 主要用于编写在各种操作系统上运行的游戏和其他媒体应用程序。它是一个用 C 语言编写的跨平台多媒体库。GLEWOpenGL Extension Wrangler),如前所述,允许我们轻松使用扩展和非核心 OpenGL 功能。

下载 SDL 库

我们将首先下载基本库开始设置。让我们首先按照以下步骤下载 SDL 库:

  1. 访问 libsdl.org/index.php,转到下载,并点击最新版本;在撰写本书时,SDL 2.0 是最新版本。

  2. 点击最新版本后,你可能想要下载开发库或运行时库。对于这个项目,建议你下载开发库。

  3. 我们将选择 Visual C++ 的版本,即 SDL2-devel-2.0.8-VC.zip。点击文件名并下载。

  4. 下载完文件后,解压并将其放入我们在前面章节中创建的OpenGL文件夹内的SDL文件夹中。

  5. 下载 SDL 库之后,我们继续下载 GLEW 库,但由于我们在前面的章节中已经下载了它们,所以你可以直接参考那里。

如果你想快速回顾下载 GLEW,可以参考章节开头“下载基本库”部分。

使用 SDL 和 GLEW 以绝对链接方式设置 OpenGL

按照以下步骤在 Visual Studio 中设置使用 SDL 和 GLEW 以绝对链接方式设置 OpenGL 的环境:

  1. 打开 Visual Studio 并在主页窗口中点击“创建新项目...”。

  2. 前往 Visual C++ | Windows 桌面 | Windows 控制台应用程序,将你的项目命名为 SDLOpenGL,然后点击确定。

  3. 接下来,在解决方案资源管理器窗口中右键单击项目。点击“属性”。

  4. 将弹出一个属性页窗口,点击 C/C++ | 一般,然后转到附加包含目录。点击下拉菜单,然后点击编辑,你将得到一个弹出窗口。

  5. 点击“新建”按钮,然后点击三个点。现在,你想要进入OpenGL文件夹中的 SDL。选择include,然后点击“选择文件夹”按钮。重复相同的步骤以包含 GLEW 文件。一旦两个文件都已被包含,点击“确定”按钮。

  6. 现在,再次在“属性页”窗口中,我们将转到链接器 | 一般,然后转到附加库目录。点击下拉菜单,然后点击“编辑”,你将得到一个弹出窗口。

  7. 在窗口中,点击“新建”按钮,然后点击三个点,进入 SDL 文件夹。打开lib文件夹,转到 x86(实际上是一个 32 位文件),然后点击“选择文件夹”按钮。

  8. 重复相同的步骤以包含 GLEW 库。打开lib文件夹,然后双击Release文件夹,选择 Win32,然后点击“选择文件夹”按钮。一旦添加了这两个库,点击“确定”按钮。

  9. 接下来,我们将转到链接器 | 输入,然后转到附加依赖项 点击下拉菜单,然后点击“编辑”,输入opengl32.lib。然后,输入glew32s.lib。如果您不想静态链接库,可以只删除s。接下来,输入SDL2.libSDL2main.lib,然后点击“确定”。

  10. 然后,点击“应用”按钮。

使用 SDL 和 GLEW 设置 OpenGL,并使用相对链接

在本节中,我们将查看如何使用 SDL 和 GLEW 作为创建渲染窗口的提供者来设置 OpenGL。按照以下步骤操作:

  1. 点击“创建新项目...”,然后转到 Visual C++。选择 Windows 控制台应用程序,并将其命名为类似SDLApp的名称。

  2. 然后,在“新项目”窗口中,点击“浏览...”按钮。转到您在桌面上创建的OpenGL文件夹,并将下载的库放入外部库中。只需选择文件夹,然后点击“确定”。

  3. 现在,我们将在“解决方案资源管理器”窗口中的项目上右键单击。转到“添加 | 新项”,你将得到一个“添加新项”窗口。选择 C++文件,因为这将是我们的主入口点;让我们将其命名为main.cpp,然后点击“添加”按钮。

  4. 接下来,再次在“解决方案资源管理器”窗口中右键单击项目。点击“属性”。

  5. 将会弹出一个“属性页”窗口。点击 C/C++ | 一般,然后转到附加包含目录。点击下拉菜单,然后点击“编辑”。

  6. 然后,点击“新建”按钮,在文本框中输入$(SolutionDir)。此命令指的是包含我们的.sln文件的文件夹。因此,如果我们指定路径中的文件夹,并且每当我们在项目中做新的操作时,它都会相对链接到该文件所在的位置。

  7. 要链接包含文件,添加路径,如以下截图所示:

图片

  1. 接下来,我们将链接库。因此,转到链接器 | 通用,然后转到附加库目录。点击下拉菜单,然后点击编辑。点击新建并添加路径,如以下截图所示,然后点击确定,并点击应用:

图片

  1. 接下来,我们将链接.lib文件。因此,转到下拉菜单并点击编辑。现在,只需输入opengl32.lib。然后,我们将输入glew32s.lib。接下来,我们将输入SDL2.libSDL2main.lib,然后点击确定。

  2. 然后,点击应用按钮。

将 DLL 文件添加到项目中

如前几节所述,在完成设置之前,我们必须将动态链接库复制到我们的项目中。按照以下步骤进行操作:

  1. 转到C:\OpenGL\SDL\lib\x86并复制SDL2.dll动态链接库,如以下截图所示:

图片

SDL2.dll 动态链接库

  1. 现在,转到你的系统中main.cpp文件所在的位置,并将动态链接库粘贴在那里。我们还需要从GLEW文件夹的bin文件夹中复制并粘贴glew32.dll文件。

在 Mac 上使用 SDL 设置 OpenGL

在这里,我们将查看如何在 Mac 系统上使用 SDL 设置 OpenGL。我们将首先在你的系统上下载必要的库。如前几节所示,我们将使用 Homebrew 下载软件包和库。

下载 SDL 和 GLEW 库

在终端中,输入以下命令以下载和安装 SDL 库:

brew install sdl2

现在,只需按Enter键,SDL 库将被下载到你的系统上。接下来,我们将下载 GLEW 库,但由于我们已经在上一节中下载了它,你可以参考那里。如果你想快速回顾下载 GLEW,可以参考为 Mac 下载 GLFW 和 GLEW 库部分。

使用 SDL 设置 Xcode 的 OpenGL

按照以下步骤操作:

  1. 打开 Xcode 并点击创建一个新的 Xcode 项目。

  2. 转到 OS X | 应用程序,然后选择命令行工具,并点击下一步。

  3. 你将看到以下窗口。填写必要的详细信息,如截图所示,并确保在语言选项中选择了 C++:

图片

项目详情

  1. 然后,设置你想要存储和保存项目的位置,然后点击创建按钮。

  2. 接下来,点击你的项目并转到构建设置。在构建设置中,转到搜索路径部分并点击头文件搜索路径。然后,点击加号并输入/usr/local/include。这将允许我们在main.cpp中包含 GLEW 和 SDL 头文件。

  3. 现在转到构建阶段,然后点击链接二进制与库,并点击加号按钮。在搜索栏中输入opengl,选择OpenGL.framework,然后点击添加按钮。

  4. 再次点击+按钮,然后点击添加其他...现在,按Cmd + Shift + G,它将打开一个go-to文件夹搜索栏。在其中,键入/usr/local。然后点击 Cellar,进入 glew | lib 文件夹,选择不带小箭头的libGLEW.1.12.0.dylib,然后点击打开。

  5. 再次点击+,然后点击添加其他...按Cmd + Shift + G并键入/usr/local。现在转到 Cellar,进入 sdl | lib。选择非别名的libSDL2-2.0.0.dylib,然后点击打开按钮。

执行所有步骤后,我们的项目现在已设置好,可以在 Mac 上使用 SDL 和 GLEW 以及 OpenGL。我们现在可以转到main.cpp文件,并开始编写创建 OpenGL 渲染窗口的代码。

使用 SDL 创建 OpenGL 渲染窗口

执行以下步骤以了解如何使用 SDL 创建渲染窗口:

  1. 让我们去 Visual Studio 或 Xcode 中的main.cpp文件,开始编写代码。首先,我们需要包含iostream;这将用于记录任何错误:
#include <iostream> 
  1. 然后,我们将包含其他必要的头文件,如下所示:
#include <SDL.h> 

#include <GL/glew.h> 

#include <SDL_opengl.h>  
  1. 接下来,我们将使用GLint创建一个常量变量:
const GLint WIDTH = 800, HEIGHT = 600;

使用GLint的原因相当简单:在不同的编译器上,普通的int可能有不同的大小,而GLint始终是一致的。WIDTHHEIGHT变量将存储我们窗口的大小。

  1. 然后,我们将设置我们的主入口点:
int main(int argc, char *argv[]) 
{ 

你可能已经注意到我们传递了argc整数和*argv []作为char。这些是参数计数和参数值,SDL 需要它们来运行代码,否则在运行时你会得到错误。

  1. 接下来,我们将使用SDL_Init()初始化 SDL,并将SDL_INIT_EVERYTHING传递给它,以确保我们正在初始化 SDL 库的每个部分:
   SDL_Init(SDL_INIT_EVERYTHING); 
  1. 然后,我们将设置一些属性,这些属性本质上是我们将为我们的窗口设置的属性:
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,     SDL_GL_CONTEXT_PROFILE_CORE); 

因此,我们可以使用 SDL 的 OpenGL 的三个主要配置文件:

  • ES,即嵌入式系统,用于移动设备等设备

  • 这是核心配置文件,用于现代 OpenGL

  • 然后是兼容性配置文件,它允许你使用较旧的 OpenGL 版本,并确保最大兼容性。

对于我们的项目,我们将使用核心配置文件。

  1. 接下来,我们将设置更多属性,如下所示:
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); 
  1. 一旦所有属性都已声明,我们将声明 SDL 窗口,如下所示:
SDL_Window *window = SDL_CreateWindow("OpenGL", 0, 0, WIDTH, HEIGHT, SDL_WINDOW_OPENGL); 

之前的代码包含我们窗口的名称,OpenGL。然后,我们将窗口位置设置为(0, 0)。为了设置窗口的宽度和高度,我们将使用之前声明的WIDTHHEIGHT值。使用这些值的优点是,如果我们稍后更改它们,这些值将得到更新。

  1. 接下来,对于上下文,我们只需要提供之前创建的窗口变量:
SDL_GLContext context = SDL_GL_CreateContext(window); 

// Set this to true so GLEW knows to use a modern approach to
retrieving function pointers and extensions 
glewExperimental = GL_TRUE; 
  1. 现在,我们将初始化 GLEW 并通过在 if 语句中检查条件来确保它已经被初始化。如果没有初始化,我们将在控制台中通知用户或开发者:
// Initialize GLEW to setup the OpenGL Function pointers 
if (GLEW_OK != glewInit()) 
{ 
      std::cout << "Failed to initialize GLEW" << std::endl; 
      return EXIT_FAILURE; 
}  
  1. 现在,我们将设置 OpenGL 视口,如下所示:
// Define the viewport dimensions 
glViewport(0, 0, WIDTH, HEIGHT); 

在前面的代码行中,我们设置了初始坐标从 00WidthHeight。您在这里检索到的值将是我们窗口相对于屏幕的准确表示,因为您可能有更高或更低的像素密度屏幕。接下来,我们将创建一个窗口事件,如下所示:

SDL_Event windowEvent; 
  1. 现在,我们将创建我们的游戏循环:
while (true) 
{ 
      if (SDL_PollEvent(&windowEvent)) 
      { 
             if (windowEvent.type == SDL_QUIT) break; 
      } 

      // Clear the colorbuffer 
      glClearColor(0.2f, 0.3f, 0.3f, 1.0f); 
      glClear(GL_COLOR_BUFFER_BIT); 

      // draw OpenGL 

      SDL_GL_SwapWindow(window); 
} 

   SDL_GL_DeleteContext(context); 
   SDL_DestroyWindow(window); 
   SDL_Quit(); 

   return EXIT_SUCCESS; 
} 

在前面的代码中,我们将 while 设置为 true 以保持循环在应用程序打开期间持续运行。如果发生某些情况,比如用户关闭应用程序,我们将退出 while 循环并进行一些清理。当循环运行时,我们将检查窗口事件并传递窗口的引用。我们还将检查窗口是否正在关闭,如果是的话,我们将退出循环。现在,在两个 if 语句之外,我们将尝试使用 glClearColor 语句清除屏幕。ClearColor 语句并不是必需的。我们添加它是因为我们可能最终会得到一个黑色背景,因为我们目前没有绘制任何形状或纹理。我们将使用以下参数将颜色添加到窗口中:0.2f0.3f0.3f1.0f。这些值介于 0 和 1 之间;它们与 0 到 255 非常相似。这些是红色、绿色、蓝色和 alpha 值。接下来,我们将使用 glClear 清除屏幕。最后,我们将执行的操作是 SDL_GL_SwapWindow。如果存在双缓冲,它将交换窗口;如果没有,则不会。然后,我们将进行一些清理并使用 EXIT_SUCCESS 退出代码。

现在,让我们运行此代码并检查输出。您将得到与前面章节中相同的 OpenGL 窗口。

在 Windows 上使用 SFML 设置 OpenGL

在本节中,我们将研究如何在 Windows 机器上使用 SFML 和 GLEW 设置 OpenGL。但是,首先,让我们了解 SFML 是什么。SFML 是一个简单且快速的多媒体库。它是一个为跨平台使用而设计的软件开发库,旨在为系统上的各种多媒体组件提供编程接口。它允许您执行诸如处理或渲染窗口、绘制我们的 OpenGL 以及处理事件(如各种输入)等操作,它还允许我们处理纹理。

下载 SFML 库

请通过访问www.sfml-dev.org/index.php将 SFML 库下载到您的系统上。然后,转到下载,点击 SFML 2.5.0,然后选择与您的 Visual Studio 版本和系统兼容性匹配的任何 Visual C++版本,并相应地点击链接。文件将以 ZIP 文件的形式下载到您的系统上。接下来,转到OpenGL文件夹(我们在前面的章节中创建的),并在其中创建一个名为SFML的文件夹以提取和放置我们的 SFML 文件。

将 SFML 和 GLEW 库链接到项目中

将 SFML 和 GLEW 库以绝对或相对链接方式链接到我们的项目的步骤与我们之前章节中讨论的类似。唯一的区别将在于我们链接.lib文件的步骤。为此,转到“附加依赖项”,在文本框中仅输入opengl32.lib。然后,我们将输入glew32s.lib。要链接 SFML 库,我们将输入sfml-graphics.libsfml-system.libsfml-window.lib,然后点击确定。

将 DLL 文件添加到项目中

如前几节所示,在开始编码之前,我们需要将动态链接库放入我们的项目中。为此,转到C:\OpenGL\SFML\bin\并复制sfml-graphics-2.dllsfml-system-2.dllsfml-window-2.dll,并将它们粘贴到您的系统中main.cpp文件所在的位置。我们还将从GLEW文件夹的bin文件夹中复制并粘贴glew32.dll文件到这里。

通过这样,我们就准备好使用 SFML 编写 OpenGL 渲染窗口了。

在 Mac 上使用 SFML 设置 OpenGL

将 SFML 和 GLEW 库下载并链接到我们的项目的步骤将与之前章节中讨论的将 GLFW 和 SDL 库链接到 Mac 系统上的项目类似。

设置过程完成后,让我们继续编写我们的 OpenGL 渲染窗口代码。

使用 SFML 创建 OpenGL 渲染窗口

检查以下步骤:

  1. 在 Visual Studio 或 Xcode 中转到您的main.cpp文件,并开始输入以下代码:
#include <iostream> 
  1. 在这里,我们将 GLEW 和 SFML 库包含到我们的项目中:
#include <GL/glew.h> 

#include <SFML/Window.hpp> 

const GLint WIDTH = 800, HEIGHT = 600; 

在前面的代码行中,我们定义了GLint常量。我们创建全局变量的原因是为了能够轻松地在代码的任何需要的地方使用它们,无论是最初创建窗口还是操纵某种形状。

  1. 接下来,让我们定义我们的入口点:
int main( ) 
{ 
   sf::ContextSettings settings; 
   settings.depthBits = 24; settings.stencilBits = 8; 

在前面的代码行中,我们为我们的应用程序和渲染窗口定义了一些设置:

settings.majorVersion = 3; 
settings.minorVersion = 3; 
settings.attributeFlags = sf::ContextSettings::Core; 

在这里,我们在前面的代码行中定义的majorVersionminorVersion是为了设置 OpenGL 的版本。在这里,我们通过将minorVersionmajorVersion设置为 3.3 来设置版本为 3.3。如果你希望设置其他版本,你必须相应地进行更改。majorVersion位于小数点左侧,而minorVersion位于小数点右侧。然后,我们通过将ContextSettings设置为Core来定义我们正在使用核心现代 OpenGL。

  1. 接下来,你想要定义sf::Window。在这里,我们将放置sf::VideoMode,并将WIDTHHEIGHT32设置为像素深度。然后,我们将添加OpenGL SFML作为窗口的标题。接着,我们添加sf::Style::Titlebarsf::Style::Close以使窗口具有标题栏和关闭按钮:
sf::Window window( sf::VideoMode( WIDTH, HEIGHT, 32 ), "OpenGL     SFML", sf::Style::Titlebar | sf::Style::Close, settings ); 
  1. 现在,我们将尝试通过将其设置为TRUE来初始化 GLEW,如果初始化失败,我们将向开发者显示Failed to initialize GLEW消息。然后,我们将执行return EXIT_FAILURE因为初始化失败了:
   glewExperimental = GL_TRUE; 

   if ( GLEW_OK != glewInit( ) ) 
   { 
      std::cout << "Failed to initialize GLEW" << std::endl; 

      return EXIT_FAILURE; 
   } 

   bool running = true; 
  1. 接下来,我们将创建一个while循环并在其中定义某些条件:

while ( running ) 
{ 
   sf::Event windowEvent; 

   while ( window.pollEvent( windowEvent ) ) 
   { 
      switch ( windowEvent.type ) 
      { 
      case sf::Event::Closed: 
         running = false; 

         break; 
      } 
  }  

在前面的while循环中,我们声明如果窗口关闭,我们将停止运行我们的应用程序并退出循环。

  1. 然后,我们将为我们的窗口添加一些颜色并定义一个绘图空间:

      glClearColor( 0.2f, 0.3f, 0.3f, 1.0f ); 
      glClear( GL_COLOR_BUFFER_BIT ); 

      // draw OpenGL 

      window.display( ); 
   } 

   window.close( ); 

   return EXIT_SUCCESS; 
  }
}   

让我们运行我们的代码并检查是否有任何错误。如果没有错误弹出,我们将得到一个渲染窗口作为输出,类似于我们在前面的章节中看到的那样。

摘要

在本章中,我们讨论了如何使用各种库设置 OpenGL:GLFW、GLEW、SDL 和 SFML。我们学习了如何在 Windows 和 Mac 上设置我们的 OpenGL 项目。此外,我们还讨论了如何使用绝对或相对链接将库链接到我们的项目中。然后,我们创建了渲染窗口来显示 OpenGL 图形。

在下一章中,我们将学习如何使用 OpenGL 绘制三角形和矩形等形状。此外,我们还将讨论如何将颜色和纹理应用到形状上。

第二章:绘制形状和应用纹理

上一章全是关于设置我们的项目以使用不同类型的库,如 GLFW、GLEW、SMFL 和 SDL。在本章中,我们将超越设置部分,学习实现一些真正酷的 OpenGL 功能。我们将学习着色器以及如何使用它们来创建各种形状。然后,我们将继续学习如何创建一个单独的着色器文件并在我们的代码中引用它。我们还将讨论如何使用 SOIL 库将不同的纹理应用到形状上。

在本章中,我们将详细介绍以下主题:

  • 学习使用着色器绘制三角形

  • 创建一个单独的着色器文件并在主代码中引用它

  • 使用 SOIL 库绘制矩形并应用纹理

本章将引导你进入 OpenGL 编程,你将学习很多与之相关的概念。

在我们开始编码之前,有一些事情我们需要理解。首先,从现在开始我们将编写的代码是平台和框架无关的。所以,无论你是在 Mac 上使用 Xcode 还是 Windows 上的 Visual Studio,任何平台上的 OpenGL 代码都将相同,因为 OpenGL 是一种平台无关的编程语言。其次,对于本章,我们将使用 GLFW 库。由于我们不会编写任何 GLFW 特定的代码,本章中的代码将适用于 SFML、SDL 或任何其他你希望使用的库。

因此,让我们开始吧。

你可以在 GitHub 上的Chapter02文件夹中找到本章的所有代码文件。本书的序言中可以找到 GitHub 链接。

绘制三角形

在本节中,我们将探讨如何在 OpenGL 中使用 GLFW 库绘制三角形。首先,让我们回到上一章中我们使用 GLFW 库创建 OpenGL 渲染窗口的代码文件,并对它进行必要的修改。让我们看一下以下步骤,以了解绘制三角形所需的代码:

  1. 我们将首先在我们的代码中包含必要的头文件:
#include <iostream>
// GLEW
#define GLEW_STATIC
#include <GL/glew.h>
// GLFW
#include <GLFW/glfw3.h>
// Window dimensions
const GLuint WIDTH = 800, HEIGHT = 600;
  1. 要在现代 OpenGL 中创建形状,我们需要创建着色器。因此,让我们首先在我们的代码中添加一些着色器。首先,我们将添加一个常量,GLchar *,我们将称之为vertexShaderSource。这将是一个字符串,其版本将是330 核心
// Shaders
const GLchar* vertexShaderSource = "#version 330 core\n"

330 核心定义了 OpenGL 3.3 版本的着色器语言版本。如果你使用的是 OpenGL 4.0 版本,那么着色器语言版本不一定是440;它可能是其他版本。通过在互联网上查找,你可以了解应该使用哪个着色器版本。

  1. 我们在上一行代码中提到的vertexShaderSource将仅处理我们三角形的位置定位,我们将如下定义:
"layout (location = 0) in vec3 position;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x, position.y, position.z, 1.0);\n"
"}\0";
  1. 在下面的代码中,我们将有另一个着色器源,即fragmentShaderSource。这将处理我们三角形的颜色纹理。目前,我们在着色器中明确设置的颜色值仅在vec4变量中:
const GLchar* fragmentShaderSource = "#version 330 core\n"
"out vec4 color;\n"
"void main()\n"
"{\n"
"color = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";

在前面的代码中,我们将vec4变量的值赋为1.0f0.5f0.2f1.0f,这些是红色、绿色、蓝色和 alpha 值。我们在这里定义的颜色范围在 0 到 1 之间,0 表示关闭,1 表示全强度;这与 RGB 颜色值在 0 到 255 之间的范围非常相似。

  1. 接下来,在以下代码行中,我们将定义我们的渲染窗口,正如前一章所讨论的,只需看一下以下代码进行审查:
 // The MAIN function, from here we start the application and run the game loop
 int main()
 {
 // Init GLFW
 glfwInit( );

 // Set all the required options for GLFW
 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 );

 glfwWindowHint( GLFW_RESIZABLE, GL_FALSE );

 // Create a GLFWwindow object that we can use for GLFW's functions
 GLFWwindow *window = glfwCreateWindow( WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr );

 int screenWidth, screenHeight;
 glfwGetFramebufferSize( window, &screenWidth, &screenHeight );

 if ( nullptr == window )
 {
 std::cout << "Failed to create GLFW window" << std::endl;
 glfwTerminate( );

 return EXIT_FAILURE;
 }

 glfwMakeContextCurrent( window );
// Set this to true so GLEW knows to use a modern approach to retrieving function pointers and extensions
 glewExperimental = GL_TRUE;
 // Initialize GLEW to setup the OpenGL Function pointers
 if ( GLEW_OK != glewInit( ) )
 {
 std::cout << "Failed to initialize GLEW" << std::endl;
 return EXIT_FAILURE;
 }

 // Define the viewport dimensions
 glViewport( 0, 0, screenWidth, screenHeight );
  1. 现在,在while循环之前,我们将添加一行代码来定义我们的着色器。让我们首先将以下代码添加到我们的程序中:
 // Build and compile our shader program
 // Vertex shader
 GLuint vertexShader = glCreateShader( GL_VERTEX_SHADER );
 glShaderSource( vertexShader, 1, &vertexShaderSource, NULL );

在前面的代码行中,我们为vertexShader创建了一个变量,并使用glShaderSource()定义了着色器的源。为此函数,我们传递了参数,将1作为vertexShaderSource的引用,并将最后一个参数暂时传递为NULL

  1. 接下来,我们将使用glCompileShader()编译着色器,并将vertexShader传递给它。然后,我们将使用GLint success检查任何编译错误。我们将以日志的形式向开发者显示这些编译错误。因此,我们定义了一个char变量infoLog,它将是一个包含 512 个项目的数组:
glCompileShader( vertexShader );
 // Check for compile time errors
 GLint success;
 GLchar infoLog[512];
  1. 然后,我们将向我们的代码中添加glGetShaderiv()函数。它将在params中返回我们的着色器对象的参数值。为此函数,我们将传递参数vertexShader、编译状态GL_COMPILE_STATUS,然后传递&success
glGetShaderiv( vertexShader, GL_COMPILE_STATUS, &success );
  1. 接下来,我们将使用if语句检查我们的着色器是否成功编译。如果它没有成功编译,将会生成一个着色器日志,并让开发者了解编译错误。为了显示错误,我们将添加glGetShaderInfoLog()函数,并在其中传递参数vertexShader512NULLinfoLog,然后添加

    std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"并输出infoLog,以便我们可以更深入地了解它:

if ( !success )
 {
 glGetShaderInfoLog( vertexShader, 512, NULL, infoLog );
 std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
 }
  1. 现在,我们将对片段着色器做同样的操作,看一下以下突出显示的代码行,以了解对片段着色器所做的更改:
 // Fragment shader
 GLuint fragmentShader = glCreateShader( GL_FRAGMENT_SHADER );
 glShaderSource( fragmentShader, 1, &fragmentShaderSource, NULL );
 glCompileShader( fragmentShader );

 // Check for compile time errors
 glGetShaderiv( fragmentShader,GL_COMPILE_STATUS, &success );

 if ( !success )
 {
   glGetShaderInfoLog( fragmentShader,512, NULL, infoLog );
   std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog <<    std::endl;
 }
  1. 然后,我们将链接着色器。为此,我们将创建一个名为shaderProgram的变量,并在glCreateProgram();中引用它。glCreateProgram()创建一个空的程序对象,并通过返回一个非零值来引用它。程序对象是一个可以附加着色器对象的实体。

  2. 然后,我们将定义glAttachShader();函数来附加我们的着色器。在那里,我们将传递shaderProgram,这是我们之前步骤中刚刚创建的变量。然后我们将传递要附加到其上的着色器。所以,第一个我们将传递的是vertexShader,然后我们将附加fragmentShader。然后,我们将定义glLinkProgram();函数,并将shaderProgram链接到它。看看以下代码以了解描述:

 // Link shaders
 GLuint shaderProgram = glCreateProgram( );
 glAttachShader( shaderProgram, vertexShader );
 glAttachShader( shaderProgram, fragmentShader );
 glLinkProgram( shaderProgram );
  1. 接下来,我们将检查任何链接错误,始终记得检查你代码中的任何错误。我们将如下检查错误:
 // Check for linking errors
 glGetProgramiv( shaderProgram, GL_LINK_STATUS, &success );

 if ( !success )
 {
 glGetProgramInfoLog( shaderProgram, 512, NULL, infoLog );
 std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
 }
glDeleteShader( vertexShader );
glDeleteShader( fragmentShader );

在上一行代码中,我们定义了glGetProgramiv();并引用了我们的shaderProgram,因为我们将会检查代码中是否存在任何错误。然后,我们将检查链接状态并将结果分配给成功。接下来,我们检查着色器的链接是否成功。如果链接不成功,我们将基本上与之前几行代码做相同的事情;也就是说,我们将生成错误日志。

我们定义了glGetProgramInfoLog ();函数,并在其中传递了参数,例如shaderProgram,因为这是我们检查错误时需要检查的内容。然后我们传递了512作为项目数量,NULL数组以及infoLog,因为这是我们打算分配任何错误日志的地方。然后我们输入了需要显示给开发者的错误信息。

因此,现在我们已经检查了在链接着色器程序时是否有任何错误,我们实际上可以删除顶点和片段着色器,因为我们将不再使用它们,因为它们现在是我们的着色器程序的一部分。所以,我们输入了glDeleteShader();函数并引用了顶点和片段着色器。

  1. 接下来,我们要做的是定义顶点数据,以便我们能够根据位置实际绘制三角形:
 // Set up vertex data (and buffer(s)) and attribute pointers
 GLfloat vertices[] =
 {
      -0.5f, -0.5f, 0.0f, // Left
      0.5f, -0.5f, 0.0f,  // Right
      0.0f, 0.5f, 0.0f    // Top
 };

在前面的代码中,如果你想绘制一个四边形,你必须定义四个顶点。

在上一行代码中,我们首先定义了一个浮点数组vertices[],并在其中定义了我们的左、右和顶部坐标。

对于我们定义的坐标,在 OpenGL 中默认情况下,如果你没有明确设置它们,你的屏幕值范围在-11之间。所以,值0位于中间,0.5是中间的 25%,或者 75%是远离左侧。在后面的章节中,我们将探讨如何更改该系统,使其实际上使用更多的屏幕。

  1. 现在我们已经创建了vertices[]数组,我们需要做的是创建顶点缓冲对象VBO)和顶点数组对象VAO)。我们首先定义GLuint变量VBOVAO。然后,我们将通过简单地输入glGenVertexArrays();来生成顶点数组,在这个函数中,我们将传递1和一个对 VAO 的引用。接下来,我们将通过定义函数glGenBuffers();来生成缓冲区,并将1和引用VBO传递给它。
 GLuint VBO, VAO;
 glGenVertexArrays( 1, &VAO );
 glGenBuffers( 1, &VBO );
  1. 然后,我们将绑定顶点数组对象,然后绑定并设置顶点缓冲区,让我们继续。我们将添加glBindVertexArray();函数并将VAO传递给它。然后,我们将添加glBindBuffer();函数并将GL_ARRAY_BUFFERVBO传递给它。接下来,我们将添加glBufferData();函数并将GL_ARRAY_BUFFERsize()传递给它。由于我们将以动态方式检查顶点的尺寸,因此我们传递了函数size(),并将要绘制的顶点传递给此函数,然后最终我们将传递GL_STATIC_DRAW。所以,这是我们用来绘制好东西的缓冲区数据:
// Bind the Vertex Array Object first, then bind and set vertex buffer(s) and attribute pointer(s).
 glBindVertexArray( VAO );

 glBindBuffer( GL_ARRAY_BUFFER, VBO );
 glBufferData( GL_ARRAY_BUFFER, sizeof( vertices ), vertices, GL_STATIC_DRAW );
  1. 接下来,我们将创建顶点指针,因此我们将添加函数glVertexAttribPointer(),并将此函数的参数传递如下代码所示。然后,我们将通过输入glEnableVertexAttribArray()函数来启用顶点数组,并将0传递给它。
 glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof( GLfloat ), ( GLvoid * ) 0 );
 glEnableVertexAttribArray( 0 );
  1. 然后,我们将绑定缓冲区,将glBindBuffer()函数添加到我们的代码中。这将需要两个值:GL_ARRAY_BUFFER0.

  2. 然后在下一行,我们将添加glBindVertexArray()函数,这将是一个0。因为我们在这里正在解绑顶点数组对象。始终解绑任何缓冲区或数组以防止出现奇怪的错误是一个好习惯。请看以下代码:

glBindBuffer( GL_ARRAY_BUFFER, 0 ); 
// Note that this is allowed, the call to glVertexAttribPointer //registered VBO as the currently bound vertex buffer object so //afterwards we can safely unbind

 glBindVertexArray( 0 ); 
// Unbind VAO (it's always a good thing to unbind any buffer/array //to prevent strange bugs)

添加代码以绘制形状

下一步,我们将添加代码以绘制三角形:

  1. 我们将在 while 循环中绘制形状。我们希望在glClear()函数之后绘制它。因此,一旦屏幕被清除,在屏幕缓冲区交换之前,我们将添加glUseProgram()函数。这将指示我们正在使用哪个着色器程序,在我们的项目中,这是shaderProgram,我们将顶点和片段着色器链接到它。

  2. 然后,我们将添加glBindVertexArray();函数并将 VAO 绑定到它。

  3. 接下来,我们将调用glDrawArrays();函数,这将最终绘制我们的三角形。在glDrawArrays();函数中,我们将传递的第一个参数是模式,即GL_TRIANGLESGL_QUADGL_LINE。根据你有多少个顶点以及你试图实现的对象或形状,这将有所不同——我们将在本章的后面更深入地介绍它。传递给glDrawArrays();函数的第二个参数是0,传递的最后一个参数是3,因为我们已经将形状中的顶点数设置为3,因为它是一个三角形。

  4. 然后,添加glBindVertexArray()函数,并将0传递给它。我们只是在取消绑定它。

  5. 现在,实际上只剩下最后一件事要做:清理。一旦我们用完所有资源,我们将释放所有资源。因此,在循环外部,添加glDeleteVertexArrays()函数并分配1&VAO,然后添加glDeleteBuffers()函数来删除缓冲区。查看以下代码以了解前面的代码描述,同时观察代码中的高亮术语:

 // Game loop
 while ( !glfwWindowShouldClose( window ) )
 {
 // Check if any events have been activiated (key pressed, mouse moved //etc.) and call corresponding response functions

glfwPollEvents( );

 // Render
 // Clear the colorbuffer
 glClearColor( 0.2f, 0.3f, 0.3f, 1.0f );
 glClear( GL_COLOR_BUFFER_BIT );

 // Draw our first triangle
 glUseProgram( shaderProgram );
 glBindVertexArray( VAO );
 glDrawArrays( GL_TRIANGLES, 0, 3 );
 glBindVertexArray( 0 );

 // Swap the screen buffers
 glfwSwapBuffers( window );
 }

 // Properly de-allocate all resources once they've outlived their purpose
 glDeleteVertexArrays( 1, &VAO );
 glDeleteBuffers( 1, &VBO );

 // Terminate GLFW, clearing any resources allocated by GLFW.
 glfwTerminate( );

 return EXIT_SUCCESS;
 }

现在我们已经准备好运行我们的代码了。一旦编译无误,你将得到以下三角形作为输出:

抽象着色器

让我们看看本节中的着色器,尽管我们在创建三角形并使用着色器为其着色时已经看过着色器。在本节中,我们将把着色器代码抽象成一个顶点着色器文件和一个片段着色器文件,这样会整洁得多,也更易于重用。此外,我们还将抽象出着色器的加载,因为一旦我们抽象出这一点,我们可能根本不需要更改它,或者至少更改不会太多。进一步来说,在我们的项目中,我们只需使用这些文件来在我们的代码中加载着色器,这将使其易于使用。

创建着色器文件

按照以下步骤创建文件:

  1. 我们将首先在我们的项目 IDE 中创建两个新的空文件,并将这两个文件命名为core.vscore.frag。在这里,vs代表向量着色器文件,而frag代表片段着色器文件。

实际上,你给它们取什么名字都无关紧要,只要在引用它们时,确切地引用名称和扩展名。

  1. 然后,打开你的core.vs文件,剪切并粘贴我们在上一节中添加的VectorShaderSource代码。按照以下代码中的高亮更改进行修改:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
out vec3 ourColor;
void main()
{
 gl_Position = vec4(position, 1.0f);
 ourColor = color;
}

让我们保存这个文件,我们的向量着色器文件就创建完成了。接下来,我们将对片段着色器文件做同样的操作。

  1. 因此,让我们在我们的 IDE 中打开core.frag,并从上一节中的代码中剪切并粘贴fragmentShaderSource代码。粘贴后,按照以下代码中的高亮部分进行修改:
#version 330 core
in vec3 ourColor;
out vec4 color;
void main()
{
 color = vec4(ourColor, 1.0f);
}

保存此文件,我们现在也创建了片段着色器文件。让我们继续创建用于抽象着色器代码加载的Shader.h文件。

创建 Shader.h 头文件

现在,我们还将创建一个着色器加载文件,即Shader.h,并使用它将我们的着色器加载到代码中。按照以下步骤创建Shader.h文件:

  1. 因此,让我们在我们的项目中创建一个空的头文件,并将其命名为Shader.h

  2. 一旦创建了这个文件,打开它,并从我们在上一节中提到的代码中剪切并粘贴着色器加载代码。

  3. 我们实际上要做的就是剪切glViewport( 0, 0, screenWidth, screenHeight );代码之后的全部内容,以及顶点数组GLfloat vertices[]代码之上的内容。因为我们剪切出的代码实际上是加载我们的着色器。

  4. 然后,按照以下代码进行更改:

 #ifndef SHADER_H
 #define SHADER_H
 #include <string>
 #include <fstream>
 #include <sstream>
 #include <iostream>
 #include <GL/glew.h>

所以,在前面几行代码中,我们只是使用简单的#ifndef#define来防止它被多次包含。我们只是包含流和字符串头文件,因为它们是我们将要加载文件的地方,所以我们需要正确的头文件来加载它。然后,显然我们需要 GLEW,这假设你已经设置了 GLEW。

  1. 之后,我们有了GLuint程序,并且那里有一些注释。我们将实时构建着色器:
class Shader
 {
 public:
 GLuint Program;
 // Constructor generates the shader on the fly
 Shader( const GLchar *vertexPath, const GLchar *fragmentPath )
 {
  1. 以下变量用于存储和加载代码和着色器文件:
// 1\. Retrieve the vertex/fragment source code from filePath
 std::string vertexCode;
 std::string fragmentCode;
 std::ifstream vShaderFile;
 std::ifstream fShaderFile;
  1. 在以下代码中,我们只是在处理一些异常:
// ensures ifstream objects can throw exceptions:
 vShaderFile.exceptions ( std::ifstream::badbit );
 fShaderFile.exceptions ( std::ifstream::badbit );
  1. 在下面的代码中,我们使用了字符串流,我们要做的是打开文件,以便我们获取顶点和片段路径。使用字符串流,我们将文件读取到实际的流中。然后,因为我们不再需要它了,我们可以直接关闭它。然后,我们将它加载到我们的字符串中,并捕获任何错误。如果你之前做过 C++,这很简单,我们建议你应该熟悉 C++:
try
 {
      // Open files
      vShaderFile.open( vertexPath );
      fShaderFile.open( fragmentPath );
      std::stringstream vShaderStream, fShaderStream;
      // Read file's buffer contents into streams
      vShaderStream << vShaderFile.rdbuf( );
      fShaderStream << fShaderFile.rdbuf( );
      // close file handlers
      vShaderFile.close( );
      fShaderFile.close( );
      // Convert stream into string
      vertexCode = vShaderStream.str( );
      fragmentCode = fShaderStream.str( );
 }

catch ( std::ifstream::failure e )
 {
     std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
 }
  1. 之后,我们只是获取 C 字符串。然后,我们只是编译着色器,这我们已经做了。所以,在代码的前两行之后,我们基本上就完成了:
 const GLchar *vShaderCode = vertexCode.c_str( );
 const GLchar *fShaderCode = fragmentCode.c_str( );

 // Compile shaders

 GLuint vertex, fragment;
 GLint success;
 GLchar infoLog[512];

// Vertex Shader
 vertex = glCreateShader( GL_VERTEX_SHADER );
 glShaderSource( vertex, 1, &vShaderCode, NULL );
 glCompileShader( vertex );
 // Print compile errors if any
 glGetShaderiv( vertex, GL_COMPILE_STATUS, &success );
 if ( !success )
 {
 glGetShaderInfoLog( vertex, 512, NULL, infoLog );
 std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
 }

// Fragment Shader
 fragment = glCreateShader( GL_FRAGMENT_SHADER );
 glShaderSource( fragment, 1, &fShaderCode, NULL );
 glCompileShader( fragment );
 // Print compile errors if any
 glGetShaderiv( fragment, GL_COMPILE_STATUS, &success );
 if ( !success )
 {
    glGetShaderInfoLog( fragment, 512, NULL, infoLog );
    std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
 }

// Shader Program
 this->Program = glCreateProgram( );
 glAttachShader( this->Program, vertex );
 glAttachShader( this->Program, fragment );
 glLinkProgram( this->Program );
 // Print linking errors if any
 glGetProgramiv( this->Program, GL_LINK_STATUS, &success );
 if (!success)
 {
    glGetProgramInfoLog( this->Program, 512, NULL, infoLog );
    std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
 }

// Delete the shaders as they're linked into our program now and no //longer necessery
 glDeleteShader( vertex );
 glDeleteShader( fragment );

 }
  1. 然后这里有一条低行,只是说使用程序:
// Uses the current shader
 void Use( )
 {
 glUseProgram( this->Program );
 }
 };
#endif

因此,我们在前面的代码中使我们的着色器代码变得更加动态。接下来,我们将进入main.cpp并对其进行一些更改。

修改绘制三角形的代码

由于我们在前面的章节中创建了着色器文件和Shader.h头文件,我们现在将加载这些文件到我们的三角形代码中。为此,我们必须对我们之前编写的三角形代码进行一些更改。请查看以下步骤:

  1. 我们首先包括Shader.h头文件,因为没有它,我们实际上无法使用Shader类:
#include "Shader.h"
  1. 然后,在我们定义顶点之前,我们将添加以下高亮显示的代码:
// Build and compile our shader program
 Shader ourShader( "core.vs", "core.frag" );

对于 Xcode,这个高亮显示的代码将被以下行代码替换:

Shader ourShader( "resources/shaders/core.vs", "resources/shaders/core.frag" );

如果你没有在 Mac 上添加这一行执行我们的三角形代码,你将得到一个错误,并且三角形不会在输出窗口中生成。

发生这种情况的原因是项目文件夹中有可执行文件,但我们没有资源文件。所以,我们必须将这些文件添加到我们的项目中:

  1. 我们想要做的是在 Xcode 的项目文件夹中,右键单击它,转到新建文件夹,创建一个名为resources的文件夹。

  2. resources文件夹内,我们将创建另一个名为shaders的文件夹。然后在那里,当我们需要这些特定文件类型时,我们创建一个名为images/videos的文件夹。所以,这对未来也会很有好处。

  3. 接下来,我们将把我们的着色器文件core.vscore.frag移动到shader文件夹中。

  4. 然后,转到你的项目,转到构建阶段,然后点击加号,并点击新建复制文件阶段选项。

  5. 点击之后,你会看到一个新部分,复制文件(0 项)。打开它,确保目标设置为 Resources,然后点击其下的加号。

  6. 然后,选择resources文件夹,并点击添加按钮。

  7. 此外,我们还想对我们的vertices []数组做一些修改。你可能记得当我们创建core.vs时,我们实际上创建了一个颜色输入。所以,我们不是直接设置颜色,我们将允许某种颜色输入。为此,我们将扩展顶点数组如下:

GLfloat vertices[] =
 {
     // Positions          // Colors
     0.5f, -0.5f, 0.0f,    1.0f, 0.0f, 0.0f,  // Bottom Right
     -0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,  // Bottom Left
     0.0f, 0.5f, 0.0f,     0.0f, 0.0f, 1.0f   // Top
 };

在上述代码行中,我们实际上是通过放置红色、绿色和蓝色的值来添加颜色的。这将非常令人兴奋,因为我们正在为每个顶点分配一个颜色,接下来会发生的事情是,颜色将混合在一起,这将在我们三角形上产生一个非常惊人的效果。

  1. 接下来,我们将进入我们的位置属性和颜色属性代码,并用以下代码替换它们:
// Position attribute
 glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof( GLfloat ), ( GLvoid * ) 0 );
 glEnableVertexAttribArray( 0 );
 // Color attribute
 glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof( GLfloat ), ( GLvoid * )( 3 * sizeof( GLfloat ) ) );
 glEnableVertexAttribArray( 1 );

上述代码行将定义三角形的定位和颜色属性。在vertices []中,我们现在有六个值:3 个用于位置坐标,另外 3 个用于颜色坐标。这就是为什么我们在前面的代码中放置了 6,因为我们有6,所以每个顶点有两个值,我们添加了颜色,因此我们需要在代码中添加6

  1. 接下来,我们将删除:
glBindBuffer( GL_ARRAY_BUFFER, 0 );
  1. 然后,我们将进入 while 循环,并将glUseProgram( shaderProgram )替换为以下代码:
ourShader.Use( );

通过对代码进行上述最后微小的修改,我们现在可以运行我们的程序了。一旦成功编译且没有错误,你将在屏幕上得到以下彩色三角形的输出:

在代码中,我们已经为每个顶点添加了颜色。输出三角形的颜色已经混合在一起。这就是 OpenGL 所做的事情:它将颜色混合在一起。如果你之前搜索过 OpenGL,或者你一般对游戏开发感兴趣,你可能会遇到一些类似这样的三角形。这几乎是 OpenGL 的一个入门仪式,相当于其他编程语言中的 Hello World 代码。

加载并将纹理应用到形状上

在本节中,我们将探讨如何在我们的代码中加载纹理,并学习将这些纹理应用到我们的对象上。纹理是一种用于为对象添加细节的图像。想象一下,如果我们将木纹应用到立方体上,那么在我们的游戏世界中它将看起来像一个木箱。

对于本节,我们的对象将是一个矩形。因此,首先我们将学习如何在 OpenGL 中绘制矩形形状,然后了解如何将其纹理应用到上面。为了将纹理应用到形状上,我们更倾向于使用 SOIL 库,即简单的 OpenGL 图像库。如果您愿意,您可以使用其他库,如 libpng,它正如其名,仅支持 PNG 格式的图像。但在这个章节中,我们只学习关于 SOIL 的内容,实际上是关于 SOIL2 的。

SOIL 是一个跨平台库,它支持 Android 和 iOS 作为游戏开发的一部分。GLFW 没有任何内置的图像加载方法,这就是我们为什么要使用 SOIL 库来加载我们的纹理。此外,SOIL 有助于使我们的代码在各种平台上尽可能动态,而且使用起来也非常简单。

因此,首先让我们了解如何在 Windows 和 Mac 平台上设置我们的项目以使用 SOIL 库。

在 Windows 上设置使用 SOIL 的项目

在本节中,我们将了解如何在 Windows 平台上设置我们的项目以使用 SOIL 库。因此,我们将从下载 SOIL 库和 Premake 开始。您可能想知道,Premake 是什么?Premake 是一个用于为 Visual Studio、Xcode 等平台生成项目文件的命令行工具。

按照以下步骤了解设置过程:

  1. 打开您的网络浏览器并访问以下链接 bitbucket.org/SpartanJ。在仓库部分点击 SOIL2 选项并打开网页,然后在简介下选择最新的 SOIL 库版本的第一个分支。

我们下载 SOIL2 库的原因是因为原始的 SOIL 库实际上非常旧,并且已经很久没有更新了。

  1. 下载完成后,只需在 Google 上搜索 Premake 或访问以下链接:premake.github.io/。然后,点击下载选项。建议您下载最新的稳定分支,因此请下载 4.4 版本,这是目前稳定的版本(在撰写本书时)。

  2. 接下来,转到您下载文件的位置,并解压这两个压缩文件夹。

  3. 然后,转到 Premake 文件夹,并将 premake4.exe 复制粘贴到我们刚刚解压的 SOIL 文件夹中。

  4. 打开命令提示符,在这里你可能需要将目录路径更改为你下载并解压 SOIL 文件夹的位置。假设 C: 驱动器是所有文件下载的驱动器,并且你将 SOIL 文件夹下载并解压到该驱动器,那么你只需要在命令提示符中输入 cd,然后将 SOIL 文件夹拖放到其中。它会自动进入 SOIL 文件夹的位置。

如果你的下载文件在另一个驱动器上,那么你首先需要告诉命令提示符将其切换到那个特定的驱动器。要做到这一点,只需输入驱动器的实际字母,然后输入 : 并按 Enter 键,然后你可以遵循之前的拖放过程。

  1. 接下来,在命令提示符中,输入 premake4.exe——或者它被称作的任何可执行文件名称——然后输入 vs2017,然后按 Enter 键。这将生成我们的 Visual Studio 项目。

如果你使用的是较旧版本,例如 2010,你可以在 vs 命令中使用 2010。它没有 Visual Studio 新版本的命令,但如果你输入那个命令,它会提示你升级一些属性,所以不用担心。

  1. 现在,让我们回到 SOIL 文件夹,打开其中存在的 make 文件夹,然后打开 Windows 文件夹。

  2. Windows 文件夹中,你会得到一个 SOIL2.sln 文件。双击它,一旦它在 Visual Studio 中打开,可能会弹出一个升级编译器和库的窗口。只需点击 OK 按钮。

  3. 然后,在 Visual Studio 中,在右侧,你会看到一些文件名。我们只关心 soil2-static-lib 这个文件。右键单击该文件,然后点击构建选项。这将构建我们的项目。然后你可以关闭 Visual Studio。

  4. 现在,如果你回到 SOIL 文件夹,会发现又生成了更多文件夹。我们感兴趣的是 lib 文件夹。

  5. lib 文件夹内部,有一个 Windows 文件夹,其中包含我们需要的 .lib 文件。

  6. 复制那个 .lib 文件,然后前往你创建 OpenGL 项目的位置。我们将在 .sln 文件所在的位置创建一个新的文件夹,并将其命名为 External Libraries

  7. External Libraries 文件夹中,我们将创建一个名为 SOIL2 的子文件夹,并在其中创建一个名为 lib 的文件夹。

  8. 然后,在刚才创建的 lib 文件夹中,粘贴 soil2-debug.lib 文件。这样,我们将使用相对链接来链接我们的 SOIL 库。如果你了解绝对链接过程并希望使用它,你可以使用它。

  9. 现在,我们需要做的是回到 SOIL 文件夹,复制 SOIL2 文件夹中的文件,并将其粘贴到 OpenGL 文件夹内的 OpenGL_VisualStudio 文件夹中。

  10. 所以,一旦你完成了所有这些步骤,最后一件要做的事情是将soil2-debug.lib链接到我们的项目中。要使用相对链接将.lib文件链接到 Visual Studio 项目,你可以参考上一章中的“使用相对链接链接 GLFW 和 GLEW 库”部分。

通过这样,我们已经设置了我们的项目以在 Visual Studio 中使用 SOIL 库。

在 Mac 上设置使用 SOIL 的项目

在本节中,我们将了解如何在 Mac 平台上设置我们的项目以使用 SOIL 库。让我们看看以下步骤:

  1. 打开你的网络浏览器并访问以下链接:bitbucket.org/SpartanJ

  2. 在“存储库”部分点击 SOIL2 选项并打开网页,然后在“简介”部分选择最新的 SOIL 库的第一个分支。

  3. 接下来,转到你下载文件的位置并解压它们。解压后,转到src文件夹,然后进入SOIL2文件夹。然后只需将这个文件夹复制并粘贴到你的项目目录中,这个目录也包含你的main.cppshader文件。

  4. 现在,就像你通常做的那样将库添加到我们的三角形项目中。所以,我们将右键单击项目,在 Xcode 中转到“添加文件”选项,点击 SOIL,然后点击“添加”按钮。

因此,一旦我们完成了包含操作,设置部分就结束了。现在,我们将回到上一节中的代码,并对其进行修改以向我们的形状添加纹理。

将纹理应用到我们的形状上

现在我们已经设置好使用 SOIL 库,让我们转到我们的三角形代码,并对它进行必要的修改以加载我们的形状并应用纹理。请按照以下步骤操作:

  1. 因此,我们首先将 SOIL 库包含到我们的代码中;为此,请在代码开头输入以下行:
#include SOIL2/SOIL2.h

这里,我们输入了SOIL2/SOIL2.h,因为我们的库在SOIL2文件夹中。

  1. 下一步,我们将启用具有如 PNG 扩展的图像的 alpha 支持。要做到这一点,在我们定义了glViewport()之后,输入以下代码行:
// enable alpha support
 glEnable( GL_BLEND );
 glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );

在前面的代码行中,glEnable(GL_BLEND)函数将帮助我们混合形状中的图像。然后我们使用了glBlendFunc(),并传递了两个参数GL_SRC_ALPHAGL_ONE_MINUS_SRC_ALPHA。这就是我们启用 alpha 支持的方式。

  1. 接下来,我们需要修改我们的顶点,因为我们将会使用矩形形状来应用纹理,同时我们还需要添加纹理坐标。所以请查看以下顶点数组,并在你的代码中进行必要的修改:
// Set up vertex data (and buffer(s)) and attribute pointers
 GLfloat vertices[] =
 {
 // Positions         // Colors             // Texture Coords
 0.5f, 0.5f, 0.0f,    1.0f, 0.0f, 0.0f,     1.0f, 1.0f, // Top Right
 0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,     1.0f, 0.0f, // Bottom Right
-0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,     0.0f, 0.0f, // Bottom Left
-0.5f, 0.5f, 0.0f,    1.0f, 1.0f, 0.0f,     0.0f, 1.0f  // Top Left
 }; 

由于我们要绘制一个矩形,我们需要四个不同的顶点:左下角、右下角、左上角和右上角。在先前的代码中,我们添加的值实际上不在-1 和 1 之间;这些值在 0 和 1 之间,因此被称为归一化值。你可能在计算机图形学中经常听到这个术语。归一化值基本上意味着值在 0 和 1 之间。例如,如果你有一个宽度为 1280 x 1280 的图像,归一化版本是 0 到 1,如果你将值设置为 0.5,它将在 640 处,因为它在中间,0 和 1280 之间的一半是 640。这只是归一化的一个非常基本的概述。如果你想了解更多关于它的信息,请随时 Google 它。

  1. 接下来,我们要创建另一个索引数组。看看下面的代码,让我们试着理解它:
GLuint indices[] =
 { // Note that we start from 0!
 0, 1, 3, // First Triangle
 1, 2, 3 // Second Triangle
 };

在前面的数组中定义两个三角形的原因是,因为我们在这个部分绘制一个四边形,我们的矩形形状实际上需要定义两个三角形索引。看看下面的图像来理解这个概念:

图片

前面的图像展示了带有和不带有索引的三角形坐标的定义。那么,让我们来看看不带索引的部分。不带索引的情况下,要绘制一个四边形,你需要六个不同的顶点,如图所示。尽管两个三角形之间共享了两对相似的坐标,但顶点的定义并不高效。然而,使用索引方法,我们可以共享顶点。所以,就像带有索引的图像中那样,两个三角形共享同一对顶点。因此,我们将重用这些顶点,结果我们将只定义四个顶点。忽略图像中显示的编号;它与我们的情况略有不同,但原则仍然适用。如果你尝试在纸上绘制它,并将我们索引数组中得到的数字应用到实际的三角形或四边形上,它将更有意义。你可能现在不明白为什么要这样做,那是因为我们只是在绘制一个四边形。但是,想象一下,当你有一个游戏,你有成千上万的三角形,它们形成各种复杂的形状。因此,这种带有索引的方法确实变得非常方便,并提高了你代码的效率。

  1. 接下来,在我们的GLuint部分,在索引下面,我们想要创建另一个名为EBO的缓冲区,所以按照以下突出显示的行修改代码:
GLuint VBO, VAO, EBO;
  1. 我们需要为元素缓冲对象(EBO)生成缓冲区,所以输入以下突出显示的代码行,然后我们还需要绑定该缓冲区:
 glGenVertexArrays(1,&VAO);
 glGenBuffers(1,&VBO );
//Generating EBO
 glGenBuffers(1,&EBO );

 glBindVertexArray( VAO );

 glBindBuffer( GL_ARRAY_BUFFER, VBO );
 glBufferData( GL_ARRAY_BUFFER, sizeof( vertices ), vertices, GL_STATIC_DRAW );
 //Binding the EBO
 glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, EBO );
 glBufferData( GL_ELEMENT_ARRAY_BUFFER, sizeof( indices ), indices, GL_STATIC_DRAW );
  1. 接下来,我们将修改我们的位置和颜色属性,并添加一个额外的属性,即纹理坐标属性。让我们查看以下突出显示的代码,并尝试理解对其所做的修改:
 // Position attribute
 glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof( GLfloat ), ( GLvoid * ) 0 );
 glEnableVertexAttribArray(0);
 // Color attribute
 glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof( GLfloat ), ( GLvoid * )( 3 * sizeof( GLfloat ) ) );
 glEnableVertexAttribArray(1);
 // Texture Coordinate attribute
 glVertexAttribPointer( 2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof( GLfloat ), ( GLvoid * )( 6 * sizeof( GLfloat ) ) );
 glEnableVertexAttribArray( 2 );

在前面的代码中,对于位置和颜色属性,我们将 6 替换为 8,因为我们有八个坐标在我们的顶点数组中:三个用于位置,三个用于颜色,两个用于纹理坐标。然后,我们创建了另一个顶点属性指针作为纹理坐标属性。

  1. 接下来,我们将添加纹理加载代码。在我们的主代码中,在我们解绑顶点数组并在 while 循环开始之前,我们将添加纹理加载代码。

  2. 首先,我们需要创建一个 GLuint texture 变量,因为它将持有我们对纹理的引用。

  3. 接下来,我们将创建 int 变量作为 widthheight。这将定义我们的纹理的宽度和高度。

  4. 然后,我们需要添加 glGenTextures() 函数,为此,我们将 size 设置为 1,并将对纹理变量的引用放入其中。

  5. 然后,我们将使用 glBindTexture() 函数绑定纹理。请查看以下突出显示的代码:

glBindVertexArray( 0 ); // Unbind VAO

 // Load and create a texture
 GLuint texture;
 int width, height;
 // Texture
 glGenTextures( 1, &texture );
 glBindTexture( GL_TEXTURE_2D, texture );
  1. 然后,我们将设置我们的纹理参数。为此,我们将添加函数 glTexParameteri (),并将以下参数传递给此函数:

    • 我们将要设置的第一个参数是 GL_TEXTURE_2D

    • 然后,对于 name,我们将设置我们将使用的包裹类型 GL_TEXTURE_WRAP_S

    • 对于包裹,我们将添加 GL_REPEAT

建议您查看不同的包裹技术。您可以在 OpenGL API 指南中找到更多关于此的信息,如果您查看 learnopengl.comopen.gl,您将能够阅读更多关于我们所编写的所有代码行信息。

  1. 因此,我们接下来要做的是复制上一行代码,并按照代码中的突出显示进行以下更改:
// Set our texture parameters
 glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );
 glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT );
  1. 接下来,我们将设置纹理过滤。查看以下代码了解详情:
// Set texture filtering
 glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
 glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
  1. 现在,我们将把实际的纹理加载代码添加到主代码中。为此,我们将创建一个无符号字符变量 *image,它将持有对函数 SOIL_load_image() 的引用。我们将以下参数传递给此函数:

    • 第一个参数将是实际图像的路径,我们将使用它作为对象的纹理。因此,我们将放置 res/images/image1.jpg

    • 第二个参数将是图像宽度和高度的引用。

    • 对于第三个参数,我们只需将其传递为 0

    • 对于第四个参数,我们将设置加载方法,所以我们将添加SOIL_LOAD_RGBA。即使它是一个 JPEG 图像,你也总是想将其作为 alpha 图像加载。这样做的原因是,它允许我们正在编写的代码更加动态。所以如果我们用 PNG 图像替换 JPEG 图像,它仍然可以工作。如果你放入不同类型的 alpha 图像,或者没有 alpha 的图像,只要该类型由 SOIL 支持,代码就可以正常工作。

 unsigned char *image = SOIL_load_image( "res/images/image1.jpg", &width, &height, 0, SOIL_LOAD_RGBA );
  1. 接下来,我们将通过添加glTexImage2d()函数指定一个二维纹理图像,并将此函数传递以下代码中的突出显示参数:
// Load, create texture 
glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image );
  1. 我们将生成米柏(mipmap),因此我们将放置glGenerateMipmap()函数。对于这个函数,我们指定GL_TEXTURE_2D,因为我们使用的是 2D 纹理。

  2. 然后,我们将释放我们的图像数据,因为清理总是好的。所以,我们将添加SOIL_free_image_data()函数,我们将仅指定我们的图像字符数组。

  3. 然后,我们将使用glBindTexture()函数解绑纹理,并将GL_TEXTURE_2D传递给该函数,并通过传递0来解绑纹理。查看以下代码以获得清晰的理解:

//Generate mipmaps
glGenerateMipmap( GL_TEXTURE_2D );
SOIL_free_image_data( image );
glBindTexture( GL_TEXTURE_2D, 0 );

你可能会想什么是米柏(mipmap)?米柏本质上是一种纹理图像的细节级别方案。它是一种通过原始图像的 2 倍因子创建一系列小图像的方法,然后根据观察者的距离加载最接近实际显示纹理的图像。所以如果某物较远,所需的纹理不是很大。而如果它更近,则需要更大的纹理。所以,它只是帮助正确加载纹理。建议在网上快速检查以了解更多关于米柏的信息。

我们的代码还没有完成。因此,我们现在将进入 while 循环。

修改 while 循环

让我们按照以下步骤进行:

  1. 在 while 循环中,我们将在使用着色器和绑定顶点数组之间的点之间放置一些代码。我们在这里想要做的是添加glActiveTexture()函数。这个函数将帮助我们激活我们指定的纹理。

  2. 然后,我们将添加glBindTexture()函数。对于这个函数,我们将传递GL_TEXTURE_2Dtexture

  3. 接下来,我们只需添加glUniform1i()函数,并将其传递以下突出显示的参数。

// Draw the triangle
 ourShader.Use( );
 glActiveTexture( GL_TEXTURE0 );
 glBindTexture( GL_TEXTURE_2D, texture );
 glUniform1i( glGetUniformLocation( ourShader.Program, "ourTexture" ), 0 );

  1. 然后,我们将添加绘制我们的容器的代码:
 // Draw container
 glBindVertexArray( VAO );
 glDrawElements( GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0 );
 glBindVertexArray( 0 );
  1. 最后,我们需要做的就是删除元素缓冲对象(EBO)的缓冲区。所以,我们将添加glDeleteBuffers(1, &EBO);到我们的代码中:
// Properly de-allocate all resources once they've outlived their purpose
 glDeleteVertexArrays( 1, &VAO );
 glDeleteBuffers( 1, &VBO );
 glDeleteBuffers( 1, &EBO );

 // Terminate GLFW, clearing any resources allocated by GLFW.
 glfwTerminate( );

现在,如果我们运行我们的代码,代码编译过程中将出现一些错误,你将得到以下类似的输出:

图片

这并不是我们想要的;这显然不是我们想要加载到我们的形状中的。所以,让我们尝试理解这个原因。我们得到错误输出的唯一原因是我们没有更新我们的顶点和片段着色器以加载纹理。所以,让我们更新它。

更新着色器文件以集成纹理坐标

按照以下步骤对您的着色器文件进行修改:

  1. 首先,让我们转到顶点着色器,我们的core.vs文件,并对以下代码中突出显示的部分进行修改:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
layout (location = 2) in vec2 texCoord;
out vec3 ourColor;
out vec2 TexCoord;
void main()
{
 gl_Position = vec4(position, 1.0f);
 ourColor = color;
 // We swap the y-axis by substracing our coordinates from 1\. This is done because most images have the top y-axis inversed with OpenGL's top y-axis.
 // TexCoord = texCoord;
 TexCoord = vec2(texCoord.x, 1.0 - texCoord.y);
}

你可能想知道我们在TexCoord = vec2(texCoord.x, 1.0 - texCoord.y);这里做了什么。好吧,我们正在通过从1减去我们的坐标来交换 y 轴,这仅仅是因为在 OpenGL 中,大多数图像的顶部 y 轴是反转的。

  1. 接下来,转到片段着色器core.frag,并对以下突出显示的部分进行修改:
#version 330 core
in vec3 ourColor;
in vec2 TexCoord;
out vec4 color;
// Texture samplers
uniform sampler2D ourTexture1;
void main()
{
 // Linearly interpolate between both textures (second texture is //only slightly combined)
 color = texture(ourTexture1, TexCoord);
}

现在,如果我们保存更新后的着色器文件并运行我们的程序,我们将得到以下输出:

检查您的图像是否与比例完全匹配。如果看起来有点压扁,那只是因为我们为顶点定义的坐标造成的。这与纹理加载无关。在后面的章节中,我们将学习使用逻辑坐标,这样任何加载的图像都可以正确地适应。所以,这就是纹理加载的全部内容。

概述

在本章中,我们学习了如何使用着色器绘制各种形状。我们首先绘制了一个三角形并为其添加了颜色。然后,我们使用三角形的概念来绘制我们的四边形,并学习了如何对其应用纹理。

在下一章中,我们将学习如何将平移和旋转等变换应用到我们的形状上,并学习如何绘制一个立方体并对其应用纹理。我们还将探讨投影的概念:透视和正交投影,以及如何在我们的世界中实现这些投影。

第三章:变换、投影和相机

在前一章中,我们讨论了如何使用我们的库在 OpenGL 中创建形状。我们学习了如何给形状添加颜色,并讨论了如何给形状添加纹理。本章将是前一章的延续,我们将讨论如何将旋转或平移等变换应用到我们的形状上。我们还将讨论投影和坐标系统,并尝试在我们的游戏世界中实现它们。您还将了解相机类,我们将使用它来观察和导航我们在本章中创建的多个对象。

本章将涵盖以下主题:

  • 将旋转和平移等变换应用到我们的形状上

  • 游戏世界中投影和坐标系统的实现

  • 将多个对象添加到我们的游戏世界中

  • 创建和使用相机类以更好地观察对象

那么,让我们开始吧...

您可以在 GitHub 的 Chapter03 文件夹中找到本章的所有代码文件。GitHub 链接可以在书的序言中找到。

使用 GLM 进行变换

我们将探讨如何对我们的形状及其应用的纹理进行变换。为了进行变换,我们将使用 OpenGL 数学库 GLM。对于变换,我们需要使用向量和矩阵,GLM 在后台为我们处理了很多。我们只需要调用正确的方法(例如,平移或旋转),它就会为我们执行适当的矩阵和向量变换。

建议您访问 learnopengl.comopen.gl。这些网站提供了一些非常棒的资源,展示了所有不同的向量和矩阵以及如何使用它们,并且深入探讨了背后的数学原理。所以,如果您感兴趣,您绝对应该访问这些网站。

在 Windows / Mac 上设置使用 GLM 的项目

首先,我们需要在我们的系统上下载并安装 GLM 库。这与其他我们在前几章中学到的安装框架非常相似,无论是 GLFW、SDL 还是 SFML。但如果你希望复习这部分内容,请参考第二章中“在 Windows 或 Mac 上设置项目以使用 SOIL”部分,绘制形状和应用纹理。GLM 的设置过程将与该部分讨论的内容非常相似。对于后续章节,我们将假设你已经安装了 GLM 库并设置了项目。

现在 GLM 库已经安装,我们的项目也已设置好,接下来让我们对代码进行一些修改,以便我们可以变换应用到形状上的形状和纹理。在后续章节中,我们将对前一章的代码进行修改,以变换我们的形状。

因此,首先,我们需要更新我们的着色器文件。

更新着色器文件

让我们遵循以下步骤来更新着色器文件:

  1. 我们将转到我们的顶点着色器,即core.vs文件,并对代码进行以下高亮显示的修改:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
layout (location = 2) in vec2 texCoord;
out vec3 ourColor;
out vec2 TexCoord; 

uniform mat4 transform;

void main()
{
 gl_Position = transform * vec4(position, 1.0f);
 ourColor = color;
 TexCoord = vec2(texCoord.x, 1.0 - texCoord.y);
}
  1. 对于片段着色器,我们不需要做任何事情,因为我们将会以原样要求这段代码。

现在,我们可以转到main.cpp并修改我们的主代码,以将变换应用于我们的对象。

将变换应用于对象

请查看以下步骤,以了解我们需要在我们的主代码中实现哪些修改以包含变换:

  1. 要修改我们的主代码,我们首先需要在代码中包含GLM头文件,所以请在您的代码中添加以下高亮显示的包含项:
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
  1. 我们接下来要做的事情是创建我们想要应用的变换矩阵。为此,进入 while 循环,并在定义着色器和激活纹理之间的循环中,我们需要添加以下描述的代码:

    1. 首先,我们将只创建变换矩阵。因此,我们将开始输入glm::mat4,并将其命名为transform

    2. 然后,我们将添加变换的类型。对于平移变换,我们将定义为transform = glm::translate();。对于translate()函数,我们需要指定的第一个参数是我们将要使用的 4x4 矩阵。接下来,我们将指定向量,它是glm::vec3()。这需要三个参数——x、y 和 z 坐标——对于这些,我们将传递值0.5f-0.5f0.0f。记住,这些值应该在-1 和 1 之间,目前是这样的。

    3. 接下来,我们将定义旋转变换,transform = glm::rotate(),对于rotate()函数,我们将传递参数:对于matrix,我们将再次指定transform。对于angle,我们需要说明我们希望它旋转多少,因为我们不希望它只旋转一次。我们希望它持续旋转。因此,我们将添加glfwGetTime()并将其转换为GLfloatglfwGetTime()函数将是从 GLFW 初始化以来经过的时间量,我们将使用它来旋转我们的对象。

关于时间方法的更多信息,您可以访问以下链接:

对于 GLFW 时间输入,请访问www.glfw.org/docs/3.0/group__time.html

对于 SDL 时间输入,您可以使用SDL_GetTicks方法,并在wiki.libsdl.org/SDL_GetTicks上读取信息;对于 SFML,您可以使用getElapsedTime方法,并且有关更多信息,您可以访问www.sfml-dev.org/tutorials/2.5/system-time.php

我们需要调整方法的时间,以便纹理以一定的速度旋转。所以,我们将方法 glfwGetTime( ) 乘以 -5.0f。这将使物体以一个方向旋转。如果你添加一个正数,它将以另一个方向旋转。尝试调整这个值。通过增加值,你会使它旋转得更快,而通过减少值,你会使它旋转得更慢。

我们需要传递的第三个参数是 glm::vec3(),对于这个向量,你必须指定形状要围绕哪个轴旋转。所以,你可以使用 1.0f0.0f。添加 0.0f 表示你不想围绕该特定轴旋转。因此,我们将它定义为 0.0f, 0.0f, 1.0f。这意味着我们的纹理将围绕 z 轴旋转。我们选择 z 的原因是因为,目前我们并没有在 3D 中做任何事情。所以,通过围绕 z 轴旋转,它看起来就像是一个 2D 变换。

查看以下代码以获得对先前描述的清晰理解:

// Create transformations
 glm::mat4 transform;
 transform = glm::translate( transform, glm::vec3( 0.5f, -0.5f, 0.0f ) );
transform = glm::rotate( transform, ( GLfloat)glfwGetTime( ) * -5.0f, glm::vec3( 0.0f, 0.0f, 1.0f ) );   
  1. 现在我们已经创建了变换矩阵,我们可以开始应用它。因此,我们需要获取矩阵的统一位置,并设置矩阵以便我们的着色器可以使用。所以我们需要创建一个 GLint 变量 transformLocation,并将其分配给 glGetUniformLocation() 的值,并将此函数传递给 ourShader.Program 和我们的变换矩阵 "transform"。接下来,我们需要在我们的代码中定义统一矩阵,所以将 glUniformMatrix4fv() 函数添加到我们的代码中,这个函数需要一些参数。首先,是 transformLocation,然后是 1,然后是 GL_FALSEglm::value_ptr(),并在此函数中指定我们使用的变换矩阵:
// Get matrix's uniform location and set matrix
 GLint transformLocation = glGetUniformLocation( ourShader.Program, "transform" );
 glUniformMatrix4fv( transformLocation, 1, GL_FALSE, glm::value_ptr( transform ) );

如果你想了解更多关于这些参数的作用,建议你查看 learnopengl.comopen.gl,因为这些网站对这些内容进行了更深入的讲解。

现在我们已经设置完毕,准备运行。如果我们运行这个程序,我们的图像应该会在右上角旋转,如下面的截图所示:

图片

本章中使用的插图仅用于说明目的。我们不推荐你以任何方式误用这些内容。如需更多信息,请参阅本书免责声明部分中提到的出版商的条款和条件。

建议你尝试使用以下代码行来改变旋转的位置和速度:

transform = glm::rotate( transform, ( GLfloat)glfwGetTime( ) * -5.0f, glm::vec3( 0.0f, 0.0f, 1.0f ) );

投影和坐标系

在本节中,我们将探讨投影和坐标系。所以,让我们首先了解坐标系是什么?有两种类型的坐标系:左手坐标系和右手坐标系。可视化它们最好的方法是举起你的双手,如下面的图片所示:

图片

如上图所示,你双手的食指应该指向上方。你的中指应该指向你,你的拇指应该分别指向左右方向。一旦你这样做,你的手指和拇指所指的方向就是该轴的正方向。所以,食指所指的方向(向上)是正 y 轴。中指所指的方向是正 z 轴,而拇指指向正 x 轴的方向。OpenGL 使用右手坐标系,而 DirectX 使用左手坐标系。

关于坐标系统的更多信息,你可以查看以下链接:

learnopengl.com/open.gl./

既然我们已经讨论了坐标系,让我们了解 OpenGL 中不同的投影类型。我们有透视投影和正交投影。透视投影考虑深度,一般来说,每个正在制作的游戏都会考虑这一点,而正交投影则不考虑深度。所以,在正交投影中,离你更远的物体或物体的部分看起来仍然是同样的大小。如果你有两个相同的物体,其中一个离你更远,它看起来仍然是同样的大小。然而,在透视投影中,就像在现实生活中一样,离你更远的物体会看起来更小。你可能想知道,正交投影在什么情况下会有用?一个例子是建筑,当你创建一个布局设计,你想要有物体在其它物体后面,但是,因为你提供了测量数据,你不想它们的大小发生变化。

代码修改

在本节中,我们将对我们的纹理代码进行一些修改,以整合投影和坐标系到我们的游戏世界中。

我们将从更新着色器文件开始我们的修改。

修改着色器文件

查看下一步骤以了解我们需要对着色器文件进行哪些修改:

  1. 我们想要做的第一件事是进入我们的顶点着色器,并对代码进行以下突出显示的更改:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 2) in vec2 texCoord;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
 gl_Position = projection * view * model * vec4(position, 1.0f);
 TexCoord = vec2(texCoord.x, 1.0 - texCoord.y);
}

在前面的代码中,我们移除了所有的颜色值,因为我们现在只使用纹理。其次,我们也删除了统一矩阵变换,因为我们将要使用几种不同的变换方式。第三,我们创建了一个 4x4 的统一矩阵,它将成为模型矩阵。第四,我们创建了一个 4x4 的统一矩阵,它是视图矩阵,还有一个 4x4 的统一矩阵,它是投影矩阵。这三种不同类型的矩阵执行非常重要的任务。模型矩阵将局部对象坐标转换为相机坐标。投影矩阵将相机坐标转换为归一化坐标,因此坐标在 0 到 1 之间,视图矩阵将归一化坐标转换为窗口坐标。最后,我们将矩阵乘积的值赋给gl_position以实现它。

  1. 接下来,我们将转向核心片段着色器,在这里我们将移除vec3 ourColor;其余的将保持不变。删除颜色参数的原因与上一步描述的相同。

对主代码的修改

既然我们已经更新了我们的着色器文件,我们将继续对主代码进行一些修改以实现投影和坐标系。请按照以下步骤操作:

  1. 首先,我们将移除 EBO,因为我们不再使用元素缓冲对象;我们将移除任何其他元素缓冲对象的实例,并且也将移除glDeleteBuffers( 1, &EBO )。然后,我们将删除颜色属性。

  2. 现在我们已经清理了我们的项目,我们将在代码中启用深度;我们需要启用它的原因是,如果我们有 3D 对象或者离我们更远或更近的对象,我们需要考虑深度。所以,让我们进入我们的代码,在glViewport下面添加glEnable()函数;为此,我们将传递GL_DEPTH_TEST。然后,我们将进入 while 循环,在glClear()函数中我们需要指定深度以清除深度缓冲区,所以向其中添加| GL_DEPTH_BUFFER_BIT

  3. 现在我们已经在我们的应用程序中实现了深度,我们将更新代码中的索引和顶点。在本节中,我们将实现一个 3D 立方体来帮助说明透视投影和正交投影。因此,我们需要修改我们的顶点并删除我们的索引。要将更新的顶点添加到你的代码中,请参考Projections and Coordinate Systems文件夹中的main.cpp。你会观察到有两种类型的投影的顶点:正交投影和透视投影。我们将在这两者之间切换,以了解两种投影之间的区别。在更新的顶点中,我们定义了六组顶点和纹理坐标,每组对应立方体的一个面。为了参考,请查看以下提到的立方体一侧的顶点集,并尝试理解它定义的内容:

 // Positions            //Texture Coordinates    
-0.5f, -0.5f, -0.5f,     0.0f, 0.0f,
 0.5f, -0.5f, -0.5f,     1.0f, 0.0f,
 0.5f,  0.5f, -0.5f,     1.0f, 1.0f,
 0.5f,  0.5f, -0.5f,     1.0f, 1.0f,
-0.5f,  0.5f, -0.5f,     0.0f, 1.0f,
-0.5f, -0.5f, -0.5f,     0.0f, 0.0f,

在前面的代码中,每行前三个值是 x、y 和 z 位置,接下来的两个是归一化纹理坐标。我们已经在上一节中详细介绍了这些内容。建议你查看每一组值,并尝试找出它们与立方体的哪个面相关联。

  1. 接下来,一旦我们更新了顶点,我们需要对我们的属性做一些修改,所以请查看以下更新的属性,并按照我们的代码进行类似修改:
// Position attribute
 glVertexAttribPointer( 0, 3, GL_FLOAT , GL_FALSE, 5 * sizeof( GLfloat ), ( GLvoid * )0 );
 glEnableVertexAttribArray( 0 );

// TexCoord attribute
 glVertexAttribPointer( 2, 2, GL_FLOAT, GL_FALSE, 5 * sizeof( GLfloat ), ( GLvoid * )( 3 * sizeof( GLfloat ) ) );
 glEnableVertexAttribArray( 2 );

在前面的代码中,我们已将乘以顶点大小的因子更新为5,因为我们的更新顶点中每行有五个不同的信息。

  1. 在纹理加载方面,我们不会触及任何代码,因为那已经完美定义了我们的纹理。

  2. 因此,在我们解绑纹理并清理完毕之后,在while循环开始之前,我们将定义投影矩阵glm::mat4 projection;。我们首先要讨论的是透视投影,因为那通常是你会用得最多的。因此,我们将添加projection = glm::perspective();perspective()函数需要几个值:

    • 第一个值是视野,我们将使用 45 度。这在视频游戏中是一个非常常见的值。

    • 第二个值是宽高比,我们将添加screenWidth / screenHeight。这使其保持动态。我们将每个值转换为GLfloat

    • 对于第三个值(近裁剪面),我们只需添加0.1f

    • 对于第四个值(远裁剪面),我们将使用1000.0f

请查看以下突出显示的代码,以了解前面的描述:

// Unbind texture when done,so we won't accidentily mess up our texture.
glBindTexture( GL_TEXTURE_2D, 0 ); 

 glm::mat4 projection;
 projection = glm::perspective( 45.0f, ( GLfloat )screenWidth / ( GLfloat )screenHeight, 0.1f, 100.0f );

视锥体

让我们借助以下简单的视锥体图像来理解前面的代码描述:

投影中心是虚拟摄像机放置的位置。zNear是近裁剪面,我们在代码中将其定义为0.1f1000.0f值指的是远裁剪面,即zFar。这两个值意味着任何比近裁剪面近的物体都不会在屏幕上为你绘制,任何比远裁剪面远的物体也不会为你绘制,而且任何在视图视锥体之外的物体也不会为你绘制。纵横比是宽度除以高度,视场基本上是它的高度。

对 while 循环的修改

现在我们已经创建了投影矩阵,我们实际上可以开始创建模型和视图矩阵了,你需要在 while 循环中完成这个操作。让我们看看以下步骤:

  1. 因此,首先,我们将移除定义变换矩阵的代码,因为我们不再使用它。我们将把glActiveTexture()代码移动到激活着色器之前。

  2. 之后,我们将创建模型和视图矩阵,为此,在激活着色器后,我们将在 while 循环中添加以下代码。我们将从添加glm::mat4 modelglm::mat4 view矩阵开始。模型将是model = glm::rotate(),我们将在rotate()中添加一个初始的旋转。对于rotate(),我们将传递以下参数:

    • 首先,我们将传递model,它指的是模型矩阵
// Activate shader
 ourShader.Use( );

 // Create transformations
 glm::mat4 model;
 glm::mat4 view;
 model = glm::rotate( model, ( GLfloat)glfwGetTime( ) * 1.0f, glm::vec3( 0.5f, 1.0f, 0.0f ) ); // use with perspective projection

//model = glm::rotate( model, 0.5f, glm::vec3( 1.0f, 0.0f, 0.0f ) ); // use to compare orthographic and perspective projection
 //view = glm::translate( view, glm::vec3( screenWidth / 2, screenHeight / 2, -700.0f ) ); // use with orthographic projection

view = glm::translate( view, glm::vec3( 0.0f, 0.0f, -3.0f ) ); // use with perspective projection
  • 第二,对于旋转角度,我们只需传递glfwGetTime(),这将获取从开始 GLFW 到现在的时间。这显然只会不断增加,因此我们可以用这种方式提供旋转。我们将此函数转换为GLfloat,然后将其乘以1.0f。这是一个增加和减少速度的好方法,因为你只需要改变值。

关于旋转角度的更多信息,请参阅之前提供的带有 SFML、SDL 和 GLFW 链接的信息框。

  • 接下来,我们将提供的是向量 3 矩阵glm::vec3(),对于vec3(),我们将在 x 轴上使用0.5f,在 y 轴上使用1.0f,而在 z 轴上则没有旋转。这将给我们的立方体添加一个很好的效果。
  1. 接下来,我们将输入view = glm::translate()。在这里,我们将稍微移动视图。因此,在translate()中,我们首先传递我们的视图矩阵;然后我们指定我们想要的移动类型,所以我们将输入glm::vec3();然后我们将vec3()传递给0.0f0.0f-3.0f轴。因此,我们将移动摄像机,这实际上是为了我们可以真正看到正在发生的事情。否则,我们基本上会处于立方体内部。

查看以下代码以了解前面的描述:

// Create transformations
view = glm::translate( view, glm::vec3( 0.0f, 0.0f, -3.0f ) ); // use with perspective projection

现在我们已经整理好了,你可能想知道为什么我们要添加旋转和变换矩阵。这只是为了这个特定的部分,以便更好地查看投影。你可能不希望在后续部分添加旋转或变换矩阵。

  1. 接下来,为了获取统一的位置,我们将添加以下突出显示的代码行:
// Get their uniform location
 GLint modelLoc = glGetUniformLocation( ourShader.Program, "model" );
 GLint viewLoc = glGetUniformLocation( ourShader.Program, "view" );
 GLint projLoc = glGetUniformLocation( ourShader.Program, "projection" );
 // Pass them to the shaders
 glUniformMatrix4fv( modelLoc, 1, GL_FALSE, glm::value_ptr( model ) );
 glUniformMatrix4fv( viewLoc, 1, GL_FALSE, glm::value_ptr( view ) );
 glUniformMatrix4fv( projLoc, 1, GL_FALSE, glm::value_ptr( projection ) );
  1. 接下来,我们需要绘制我们的对象,所以我们将添加 glBindVertexArray(),并将传递顶点数组对象 VAO

  2. 然后,我们将添加 glDrawArrays (),首先,我们将传递:

    • 首先 GL_TRIANGLES

    • 其次,第一个顶点将从 0 开始

    • 对于计数,我们将传递 36,因为每个面有两个三角形,每个三角形有三个坐标。两个三角形产生六个坐标,所以 6 x 6 = 36。

  3. 接下来,我们将解绑它,所以我们将添加 glBindVertexArray() 并传递 0

 // Draw container
 glBindVertexArray( VAO );
 glDrawArrays( GL_TRIANGLES, 0, 36 );
 glBindVertexArray( 0 );

现在我们已经整理好了,让我们再次检查代码,然后运行它。如果你没有遇到任何错误,你将在屏幕上看到一个类似的旋转立方体,如下所示:

正交投影

现在我们来看看正交投影的形状,并理解正交投影和透视投影之间的区别。所以,我们将注释掉透视投影坐标,并从“投影和坐标系”文件夹中的 main.cpp 添加正交投影坐标。

然后,我们将转到 glm::mat4 projection; 并注释掉透视投影,并添加以下突出显示的代码行用于正交投影:

projection = glm::ortho(0.0f, ( GLfloat )screenWidth, 0.0f, ( GLfloat )screenHeight, 0.1f, 1000.0f);

你可能想知道视场和比例在哪里。我们不需要那些,因为立方体更像是以下图像中显示的盒子:

现在我们只剩下两件事需要更改,那就是模型和视图矩阵,因为这将有助于展示我们试图展示的内容。所以,注释掉透视投影模型和视图定义,并添加以下代码行:

model = glm::rotate( model, 0.5f, glm::vec3( 1.0f, 0.0f, 0.0f ) ); 
// use to compare orthographic and perspective projection

view = glm::translate( view, glm::vec3( screenWidth / 2, screenHeight / 2, -700.0f ) ); 
// use with orthographic projection

现在,让我们运行这段代码,看看我们的立方体是什么样子:

所以我们得到了我们的立方体,但看起来有点奇怪。它看起来就像一个又长又大的矩形。但实际上,面对我们的图像是立方体的前面,顶部矩形图像是立方体的顶部。我们已经旋转了立方体,但没有透视,很难判断哪个面是哪个。但让我们做一个实验,看看当我们注释掉正交的 view 并取消注释透视的 view 时会发生什么。让我们再次引入透视投影,取消注释透视数组,并注释掉正交数组。现在如果我们用透视投影运行代码,你将看到正交投影和透视投影之间的一个关键区别;看看以下图像:

现在看看前面的图像,它明显更像是一个立方体。显然,我们看不到侧面、底部或背面,但根据我们在这里看到的情况,它看起来比之前更像一个立方体。

Camera类添加到项目中

在上一节中,我们学习了如何将对象添加到屏幕上,以及如何将纹理、变换和投影应用到对象上。随着我们在本书中继续前进,我们将向屏幕添加各种对象,但当我们向其中添加更多对象,并且希望从不同的角度观察它们时,我们不希望有特定的代码来实现这一点或自由地在对象周围移动。因此,在本章中,我们将探讨实现一个Camera类,这将帮助我们自由地在我们的世界中移动,使用键盘,并从不同的角度观察这些对象。实现这个类将帮助我们改进移动风格,并借助鼠标查看玩家周围的虚拟世界。正如我们在上一章所学,我们目前拥有的只是一个我们创建的单个立方体。由于我们本章的目标是实现多个对象,我们将基本上删除描述单个立方体的顶点,并使用一个简单的顶点数组和循环来绘制多个立方体。我们不仅会看到单个立方体的移动,还会看到多个立方体,这将非常棒。

在本节中,我们将涵盖以下内容:

  • 学习如何绘制多个立方体

  • 创建Camera类并在我们的当前应用程序中实现它

在我们开始之前,只有几个先决条件。我们将使用上一章的源代码,该章是关于投影和坐标系。如果您没有源代码,您可以从本书前言中提供的 GitHub 链接下载。此外,在本章中,我们将使用 GLFW 作为框架。这意味着我们将在这里使用一些 GLFW 编码,但这实际上只是为了输入我们所做的操作。您可以自由地查看其他库的输入指南,尝试用其他库交换代码,并尝试实验。我们将在本章中定义的Camera类不会受到您实验的影响,因为它不会包含任何框架特定的代码。只有主代码会受到影響,因为它将被用来检测输入。

让我们开始吧。

创建一个 Camera.h 头文件

我们将首先为我们的Camera类创建一个头文件,因此我们将在项目中创建一个空的头文件并将其添加到我们的目标中。我们将将其命名为Camera.h。我们只会有一个头文件,因为我们将要实现的方法非常简单。但如果你想的话,也可以将其提取到一个单独的 CPP 文件中。建议你也尝试这种方式进行实验,因为这将是一个很好的学习方法。让我们开始编写我们的摄像机头文件。按照以下步骤进行:

  1. 首先,让我们删除文件中已经存在的默认代码。然后,当代码是一个更简单的版本时,添加#pragma。这并不是所有编译器都支持,但大多数编译器都会支持这一点。

  2. 然后,我们将添加#include向量。我们将使用vector类来处理诸如定位之类的操作。接下来,让我们添加#define GLEW_STATIC,因为我们将在其中使用 GLEW,它已经链接到我们的项目中。然后,我们将添加#include GL/glew.h。我们还将包含一些 OpenGL 数学库,所以让我们添加glm/glm.hpp#include glm/gtc/matrix_transform.hpp

  3. 接下来,我们将创建一个枚举来定义摄像机移动的几个可能选项。让我们添加enum Camera_Movements。这将包含FORWARDBACKWARDLEFTRIGHT,我们需要使用这些来找出用户想要将摄像机移动到哪个方向——本质上,是为了确定用户想要向哪个方向行走。

  4. 现在,我们将为摄像机的偏航、俯仰和移动速度创建一些常量值,以及灵敏度和缩放。除了缩放之外,我们不会为这些值提供方法,但你可以为其他所有值创建方法。你可以创建获取器和设置器;建议你将此作为一个额外任务来完成。这是一个很好的学习方法,你以后也能使用它们。

  5. 因此,我们将添加const。显然,目前因为我们使用了一个常量,所以你不能修改它,但如果你想要修改,那不是问题。但这些是默认值,所以你不会特别操作这个变量。你会操作 Camera 类中的变量,我们将在不久的将来创建它。所以,添加const GLfloat YAW = -90.0f;。然后添加const GLfloat PITCH = 0.0fconst GLfloat SPEED = 6.0f。这是与相机和屏幕配合得很好的速度值;你可以根据需要调整它,使其变慢或变快。值越高,速度越快,值越低,速度越慢。接下来,添加const GLfloat SENSITIVITY = 0.25f。这定义了鼠标移动的灵敏度。同样,值越高,鼠标移动越快,值越低,鼠标移动越慢。现在我们将包括const GLfloat ZOOM。缩放值是视野,所以 45 度的值非常常见。值越高,屏幕就越高。这基本上是老游戏所用的,你可以尝试一下。查看以下代码以了解前面的描述:

#pragma once
// Std. Includes
#include <vector>
// GL Includes
#define GLEW_STATIC
#include <GL/glew.h>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

// Defines several possible options for camera movement. Used as abstraction to stay away from window-system specific input methods
enum Camera_Movement
{
    FORWARD,
    BACKWARD,
    LEFT,
    RIGHT
};

// Default camera values
const GLfloat YAW        = -90.0f;
const GLfloat PITCH      =  0.0f;
const GLfloat SPEED      =  6.0f;
const GLfloat SENSITIVTY =  0.25f;  
const GLfloat ZOOM       =  45.0f;
  1. 接下来,我们将创建一个 Camera 类,然后输入public。我们将首先创建一个带有向量的构造函数,然后是一个带有标量值的构造函数。让我们从添加Camera()开始,它将接受以下代码中显示的参数:
//Constructor with vectors
Camera( glm::vec3 position = glm::vec3( 0.0f, 0.0f, 0.0f ), glm::vec3 up = glm::vec3( 0.0f, 1.0f, 0.0f ), GLfloat yaw = YAW, GLfloat pitch = PITCH ) : front( glm::vec3( 0.0f, 0.0f, -1.0f ) ), movementSpeed( SPEED ), mouseSensitivity( SENSITIVTY ), zoom( ZOOM )
  1. 然后,我们将快速实现我们的相机构造函数,所以为构造函数添加以下行:
{
     this->position = position;
     this->worldUp = up;
     this->yaw = yaw;
     this->pitch = pitch;
     this->updateCameraVectors( );
}
  1. 现在我们已经完成了这个特定的构造函数,我们将添加带有标量值的构造函数到我们的代码中。向你的代码中添加以下行:
// Constructor with scalar values
    Camera( GLfloat posX, GLfloat posY, GLfloat posZ, GLfloat upX, GLfloat upY, GLfloat upZ, GLfloat yaw, GLfloat pitch ) : front( glm::vec3( 0.0f, 0.0f, -1.0f ) ), movementSpeed( SPEED ), mouseSensitivity( SENSITIVTY ), zoom( ZOOM )
    {
        this->position = glm::vec3( posX, posY, posZ );
        this->worldUp = glm::vec3( upX, upY, upZ );
        this->yaw = yaw;
        this->pitch = pitch;
        this->updateCameraVectors( );
    }
  1. 现在我们将实现一个获取视图矩阵的 getter,因为这个将会返回使用欧拉角和lookAt矩阵计算出的视图矩阵。我们将添加glm::mat4,并将其命名为GetViewMatrix()。我们将在main.cpp中使用它,并添加以下高亮显示的代码行到这个类中:
// Returns the view matrix calculated using Eular Angles and the LookAt Matrix
    glm::mat4 GetViewMatrix( )
    {
        return glm::lookAt( this->position, this->position + this->front, this->up );
    }

这基本上就是声明我们想要看的地方;显然,我们想要向前看;并且我们想要使用向上向量,所以我们使其相对。

  1. 现在我们将处理一些键盘输入;使用这个键盘输入,我们将检测我们是在前进、后退、左转还是右转,然后我们将朝那个方向移动。所以让我们添加以下代码行:
void ProcessKeyboard( Camera_Movement direction, GLfloat deltaTime )     {

这个Camera_Movement是我们之前步骤中创建的enumGLfloat deltaTime是帧之间的时间,因此我们可以创建与帧无关的运动,因为你最不希望看到的是每秒 60 帧,突然降到 30 帧,这只有一半的速度。你显然希望它保持相同的速度。它可能看起来不那么平滑,但你仍然会得到一致的运动,这才是最重要的。

  1. 接下来,在代码文件中,我们将添加以下代码行:
// Processes input received from any keyboard-like input system. Accepts input parameter in the form of camera defined ENUM (to abstract it from windowing systems)
    void ProcessKeyboard( Camera_Movement direction, GLfloat deltaTime )
    {
        GLfloat velocity = this->movementSpeed * deltaTime;

        if ( direction == FORWARD )
        {
            this->position += this->front * velocity;
        }

        if ( direction == BACKWARD )
        {
            this->position -= this->front * velocity;
        }

        if ( direction == LEFT )
        {
            this->position -= this->right * velocity;
        }

        if ( direction == RIGHT )
        {
            this->position += this->right * velocity;
        }
    }

在前面的代码行中,我们添加了GLfloat velocity并将其赋值为movementSpeed * deltaTime。假设movementSpeed5,例如,而deltaTime0.1,那么GLfloat velocity将是0.5。因此,如果 delta time 更高,移动速度也会更高。如果 delta time 更低,移动速度也会更低。这仅仅保持了所有帧率独立。接下来,我们添加了if语句来检查用户移动的方向。如果用户向任何特定方向移动,那么position += this front * velocity,其中velocity等于我们之前计算出的值。

你可能想知道为什么我们不使用开关语句,或者为什么不使用if else/if else系列。首先,假设你同时按下了前进键和左键,那么你希望能够向西北方向移动。忽略我们朝向的方向——让我们假设我们正朝北看——你希望能够向西北方向移动。你不想需要先按下一个键,然后松开,再按另一个键。同样地,如果你正在前进,然后点击后退,你会停止。你不想需要释放按键;这就是我们使用单独的if语句的原因。

  1. 接下来,我们将处理鼠标移动。这将处理从我们的鼠标系统接收到的输入,无论是 GLFW、SDL 还是 SFML,并且它将使用偏移值来调整 x 和 y 方向。因此,我们将添加void ProcessMouseMovement()函数,并将GLfloat xOffset传递给它。这实际上是鼠标移动之间的差异,因为否则我们怎么知道我们要移动到哪里呢?我们实际上需要考虑速度。然后添加GLfloat yOffset, GLboolean constrainPitch来约束俯仰角,并将其设置为 true。现在我们将计算偏移量,并使用我们的mouseSensitivity来调整它。我们将添加以下代码行:
       xOffset *= this->mouseSensitivity;
       yOffset *= this->mouseSensitivity;

        this->yaw   += xOffset;
        this->pitch += yOffset;
  1. 现在,我们将使用 if 语句检查俯仰角是否受到限制。我们希望防止用户超出范围,这样当我们的鼠标移动时,屏幕就不会翻转,或者实际上是我们看左和看右的方式;如果我们向左移动时间过长,我们会回到起点。同样的情况也适用于右方向(即逆时针和顺时针。当你向上或向下看时,你通常只能向上看大约 90 度,这大约是头部能做的,然后向下看大约 90 度到你的脚,这又是头部能做的。你不希望能够不断地循环回到起点,因为这会导致各种各样的问题,比如陀螺仪锁定。但是,一般来说,这种运动在游戏中是不存在的,因为游戏是基于现实生活和人体限制的。所以我们将检查俯仰角。如果pitch > 89.0f,我们将俯仰角设置为89.0f。如果pitch < -89.0f,我们将俯仰角设置为-89.0f。最后,我们将通过添加this->updateCameraVectors();来更新相机向量。这将使用我们在这里定义的欧拉角更新前、右和上向量。请查看以下代码以了解前面的描述:
// Processes input received from a mouse input system. Expects the offset value in both the x and y direction.
    void ProcessMouseMovement( GLfloat xOffset, GLfloat yOffset, GLboolean constrainPitch = true )
    {
        xOffset *= this->mouseSensitivity;
        yOffset *= this->mouseSensitivity;

        this->yaw   += xOffset;
        this->pitch += yOffset;

        // Make sure that when pitch is out of bounds, screen doesn't get flipped
        if ( constrainPitch )
        {
            if ( this->pitch > 89.0f )
            {
                this->pitch = 89.0f;
            }
            if ( this->pitch < -89.0f )
            {
                this->pitch = -89.0f;
            }
        }

        // Update Front, Right and Up Vectors using the updated Eular angles
        this->updateCameraVectors( );
    }
  1. 现在我们已经处理了鼠标移动,接下来我们将处理鼠标滚轮操作,因此我们将添加void ProcessMouseScroll()函数,并将GLfloat yOffset传递给它。

    如果你想要能够检测水平滚动,可以使用xOffset。许多鼠标没有水平滚动功能,但相当多的新型鼠标,尤其是游戏鼠标和生产力鼠标,都有。但是,一般来说,你可能只想检测 y 轴上的移动——也就是说,垂直滚动。但是你可以轻松扩展这个方法和这个类,以满足你的需求。

    在你的代码中添加以下if语句:

if ( this->zoom >= 1.0f && this->zoom <= 45.0f )
        {

            this->zoom -= yOffset;
        }

        if ( this->zoom <= 1.0f )
        {
            this->zoom = 1.0f;
        }

        if ( this->zoom >= 45.0f )
        {
            this->zoom = 45.0f;
        }

现在,我们将创建一个获取缩放的 getter,因为缩放变量将是私有的。实际上,这个类中的所有变量都是私有的。我们真正创建缩放 getter 的原因仅仅是因为它是我们现在在类外使用的唯一一个。但是,如果你需要使用诸如上向量、偏航、俯仰或其他我们创建的任何变量,请随意创建适当的 getter 和 setter。因此,我们接下来将添加以下代码:

GLfloat GetZoom( )
    {
        return this->zoom;
    }
  1. 现在,我们将定义相机属性,因此我们将向我们的相机类添加以下代码行:
private:
    // Camera Attributes
    glm::vec3 position;
    glm::vec3 front;
    glm::vec3 up;
    glm::vec3 right;
    glm::vec3 worldUp;

欢迎访问learnopengl.comopen.gl,并查看这些网站以获取更深入的书面信息和一些关于我们本章讨论的所有不同变量和方法的优秀图表。

  1. 然后,我们将向我们的类中添加一些欧拉角:
// Eular Angles
    GLfloat yaw;
    GLfloat pitch;
  1. 我们一直在使用的所有这些参数最终都被创建出来了。接下来,我们将在我们的代码中添加一些摄像机选项:
// Camera options
    GLfloat movementSpeed;
    GLfloat mouseSensitivity;
    GLfloat zoom;
  1. 我们需要添加到这个类中的最后一件事是 void updateCameraVectors,当我们更新摄像机向量时,我们需要计算新的前向量,因此我们将添加 glm::vec3 front,这是它的临时存储。然后我们将添加 front.x,并将 cos ( glm::radians( this->yaw )) 乘以 cos( glm::radians( this->pitch ) ) 的值分配给它。再次强调,这里的数学计算显然非常复杂且深入,所以我们建议您查看上述链接。查看以下代码以了解将添加到 updateCameraVectors 的其他元素:
void updateCameraVectors( )
    {
        // Calculate the new Front vector
        glm::vec3 front;
        front.x = cos( glm::radians( this->yaw ) ) * cos( glm::radians( this->pitch ) );
        front.y = sin( glm::radians( this->pitch ) );
        front.z = sin( glm::radians( this->yaw ) ) * cos( glm::radians( this->pitch ) );
        this->front = glm::normalize( front );
        // Also re-calculate the Right and Up vector        this->right = glm::normalize( glm::cross( this->front, this->worldUp ) );  // Normalize the vectors, because their length gets closer to 0 the more you look up or down which results in slower movement.
        this->up = glm::normalize( glm::cross( this->right, this->front ) );
    }

通过这一行代码,我们最终完成了 Camera 类。请查看 camera.h 文件以获取完整的代码。此文件位于 Getting started 文件夹中的 camera 文件夹内。

对 main.cpp 进行修改

现在我们已经创建了我们的 Camera 类,让我们回到 main.cpp 并对其进行一些修改,例如在我们的屏幕上实现多个立方体,添加一个摄像机类,并在多个对象之间移动。

在我们的 main.cpp 中,我们将开始实现我们一直在做的输入部分。因此,我们将使用 GLFW 作为我们的输入系统,但,再次提醒,您可以自由查看之前的链接以获取有关 GLFW、SFML 和 SDL 输入系统的更多信息。

  1. 我们将从在我们的代码中包含 Camera 类开始。在代码的开始处添加 #include Camera.h

  2. 然后,在定义我们的屏幕尺寸的部分,我们将进行以下修改:

const GLuint WIDTH = 800, HEIGHT = 600;
int SCREEN_WIDTH, SCREEN_HEIGHT;
  1. 现在,让我们将所有使用的 screenWidthscreenHeight 替换为 SCREEN_WIDTHSCREEN_HEIGHT

  2. 由于我们将使用透视投影,我们需要删除所有正交投影代码,因为我们不再使用它了。

  3. 现在,在我们开始 int main 之前,我们将创建一些函数原型。将以下代码行添加到您的 main.cpp 文件中:

// Function prototypes
void KeyCallback( GLFWwindow *window, int key, int scancode, int action, int mode );
void ScrollCallback( GLFWwindow *window, double xOffset, double yOffset );
void MouseCallback( GLFWwindow *window, double xPos, double yPos );
void DoMovement( );

在前面的代码中,我们首先添加了 void KeyCallback( ); 这是从这里开始框架特定代码的地方。我们将 GLFWwindow *window 传递给这个函数,然后我们需要检查哪个键被按下,因此添加了 int keyscancodeactionmode。然后我们添加了其余的函数。在 MouseCallback( ) 中,我们传递了 double xPosdouble yPos。这些是我们鼠标在窗口中的 x 和 y 位置。我们实际上将隐藏鼠标光标以提供更沉浸式的体验。然后,我们在前面的代码中添加了一个最终的方法原型:void DoMovement。这个方法将在每一帧中被调用,并将移动我们的摄像机。即使我们没有进行任何移动,它仍然会被调用,但不会明显移动我们的摄像机。

  1. 现在,我们只需要为我们的相机初始化一些值,所以我们将添加Camera,创建一个camera()对象,并将glm::vec3()传递给它。对于vec3(),我们将传递0.0f0.0f3.0f。这些只是初始值。接下来,我们将添加GLfloat lastX,这是相机的最后位置,初始时。我们将使其等于屏幕中心,这将是我们鼠标移动的位置。我们将添加WIDTH / 2.0GLfloat lastY = WIDTH / 2.0f;。查看以下内容以了解此描述:
// Camera
Camera  camera(glm::vec3( 0.0f, 0.0f, 3.0f ) );
GLfloat lastX = WIDTH / 2.0;
GLfloat lastY = HEIGHT / 2.0;

在此之下将是一个bool类型的键数组,它将包含 1,024 种不同类型的键。我们将添加bool firstMouse = true,因为我们正在处理一种鼠标类型:

bool keys[1024];
bool firstMouse = true;

接下来,我们将添加deltatimelastframe,它们将在代码中用于确定帧之间的时间:

GLfloat deltaTime = 0.0f;
GLfloat lastFrame = 0.0f;
  1. 现在,在我们的int main中,在glfwMakeContextCurrent(window);之后,我们将添加glfwSetKeyCallback();,并将window作为参数传递给它。我们将提供我们使用的方法,即KeyCallback;然后我们将重复此行代码三次,并对其进行以下突出显示的修改:
// Set the required callback functions
glfwSetKeyCallback( window, KeyCallback );
glfwSetCursorPosCallback( window, MouseCallback );
glfwSetScrollCallback( window, ScrollCallback );

在这里,我们正在调用之前定义的函数原型。

  1. 接下来,我们想要将鼠标固定在屏幕中心,在窗口内部,所以我们将添加glfwSetInputMode();并将window传递给它。由于我们正在更改的模式是光标,我们将传递GLFW_CURSOR与值GLFW_CURSOR_DISABLED,因为我们不希望光标完全禁用。
// Options, removes the mouse cursor for a more immersive experience     glfwSetInputMode( window, GLFW_CURSOR, GLFW_CURSOR_DISABLED );
  1. 由于我们将在代码中渲染多个立方体,我们将创建一个向量数组,该数组将包含立方体的位置。这些位置只是任意位置,所以你可以稍后更改它们以进行实验。前往Camera文件夹中的main.cpp文件,并将向量数组glm::vec3 cubePositions[]复制并粘贴到你的代码中。

    接下来,我们将移动投影代码到 while 循环内部,因为我们将通过鼠标滚轮更改视场,所以如果我们实际上正在更改视场值,我们希望能够在每一帧更新投影。因此,在激活我们的着色器代码并使用纹理单元绑定纹理之后,添加投影代码并对其进行以下更改:

// Draw our first triangle

   ourShader.Use( );

   // Bind Textures using texture units
   glActiveTexture( GL_TEXTURE0 );
   glBindTexture( GL_TEXTURE_2D, texture );
   glUniform1i( glGetUniformLocation( ourShader.Program, "ourTexture1" ), 0 );

   glm::mat4 projection;
   projection = glm::perspective(camera.GetZoom( ), (GLfloat)SCREEN_WIDTH/(GLfloat)SCREEN_HEIGHT, 0.1f, 1000.0f);
  1. 在 while 循环开始后,我们将设置帧时间,所以我们将添加GLfloat currentFrame = glfwGetTime()。然后我们将添加deltaTime = currentFrame - lastFrame。这是我们检测帧间时间的方式。比如说,如果我们的当前帧在时间 100,而我们的上一帧在时间 80,那么上一帧和当前帧之间的时间将是 20——尽管它通常是一秒或毫秒。然后我们将添加lastFrame = the currentFrame,因为当我们在下一次迭代中重新启动这个 while 循环时,最后一帧将是当前帧,因为我们将在那个特定时刻有一个不同的帧。请参考以下代码来理解描述:
while( !glfwWindowShouldClose( window ) )
    {

        // Set frame time        
        GLfloat currentFrame = glfwGetTime( );
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;  
  1. 在处理完所有事件后,我们将实际处理移动,所以添加DoMovement()

  2. 现在,我们将进入代码中定义view和模型矩阵的部分,并进行以下修改:

// Create camera transformation
        glm::mat4 view;
        view = camera.GetViewMatrix( );

在前面的代码中,你可能已经注意到我们移除了模型矩阵代码,这是因为我们将把它放在一个 for 循环中,该循环将遍历我们的立方体位置数组,在不同的位置绘制对象,并使用模型生成一种随机的旋转。

  1. 在将顶点数组绑定到顶点数组对象和解除绑定之间,我们实际上将添加一个 for 循环,我们将传递一个参数作为GLuint i = 0; i < 10; i++。建议你尝试使其动态化,这样你就可以添加更多的立方体位置,并绘制更多的立方体。那将是你另一个很好的任务。我们将在 for 循环中添加以下突出显示的语句。首先,我们将为每个对象计算模型矩阵,然后在我们开始绘制之前将其传递给我们的着色器:
for( GLuint i = 0; i < 10; i++ )
        {
            // Calculate the model matrix for each object and pass it to shader before drawing
 glm::mat4 model;
 model = glm::translate( model, cubePositions[i] );
 GLfloat angle = 20.0f * i;
 model = glm::rotate(model, angle, glm::vec3( 1.0f, 0.3f, 0.5f ) );
 glUniformMatrix4fv( modelLoc, 1, GL_FALSE, glm::value_ptr( model ) );

 glDrawArrays( GL_TRIANGLES, 0, 36 );
        }

我们在前面代码中使用了一个值为20.0,因为这只是一个计算值。尝试改变这个值并看看会发生什么。你可能会找到一个更好的值。我们已经从 while 循环中复制并粘贴了glUniformMatrix4fv();。现在我们已经完成了我们的 while 循环。

  1. 现在,我们可以开始实现那些函数原型,这是在我们能够运行代码并观察输出之前要做的最后一件事。在 while 循环结束后,我们将添加void DoMovement(),这将处理我们的移动并调用 Camera 类中的适当键盘方法。所以,这个函数不会接受任何参数,但我们将添加一些 if 语句。我们想要使用W, A, S, D和箭头键,所以我们将条件作为keys[GLFW_KEY_W] || keys[GLFW_KEY_UP]传递。在 if 语句中,我们将添加camera.ProcessKeyboard(FORWARD, deltaTime);因为我们正在向前移动,并且我们将添加deltaTime,这是我们已经在 while 循环中计算过的。这是向前移动的代码。同样,我们还将为所有其他方向添加语句;请查看以下突出显示的代码:
// Moves/alters the camera positions based on user input
void DoMovement( )
{
    // Camera controls
    if( keys[GLFW_KEY_W] || keys[GLFW_KEY_UP] )
    {
        camera.ProcessKeyboard( FORWARD, deltaTime );
    }

    if( keys[GLFW_KEY_S] || keys[GLFW_KEY_DOWN] )
 {
 camera.ProcessKeyboard( BACKWARD, deltaTime );
 }

 if( keys[GLFW_KEY_A] || keys[GLFW_KEY_LEFT] )
 {
 camera.ProcessKeyboard( LEFT, deltaTime );
 }
 if( keys[GLFW_KEY_D] || keys[GLFW_KEY_RIGHT] ) 
 {
 camera.ProcessKeyboard( RIGHT, deltaTime );
 }
}
  1. 然后,我们将进行一个回调,所以我们将添加以下代码:
// Is called whenever a key is pressed/released via GLFW
void KeyCallback( GLFWwindow *window, int key, int scancode, int action, int mode )
{
    if( key == GLFW_KEY_ESCAPE && action == GLFW_PRESS )
    {
        glfwSetWindowShouldClose(window, GL_TRUE);
    }
    if ( key >= 0 && key < 1024 )
    {
        if( action == GLFW_PRESS )
        {
            keys[key] = true;
        }
        else if( action == GLFW_RELEASE )
        {
            keys[key] = false;
        }
    }
}
  1. 现在,我们可以添加MouseCallback
void MouseCallback( GLFWwindow *window, double xPos, double yPos )
{
    if( firstMouse )
    {
        lastX = xPos;
        lastY = yPos;
        firstMouse = false;
    }
    GLfloat xOffset = xPos - lastX;
    GLfloat yOffset = lastY - yPos;  // Reversed since y-coordinates go from bottom to left

    lastX = xPos;
    lastY = yPos;

    camera.ProcessMouseMovement( xOffset, yOffset );
} 
  1. 然后,我们将添加 void ScrollCallback(),并将以下参数传递给它:GLFWwindow *window, double xOffset, double yOffset

在那个方法中,我们将添加以下代码:

camera.ProcessMouseScroll( yOffset );

现在,我们已经准备好查看这个是否工作,所以运行它。一旦编译无误,你将看到以下输出:

我们已经创建了多个立方体,并且可以四处查看和移动。我们可以使用 WASD 键以及箭头键来移动。因此,我们不仅可以向前和向后移动,还可以向前和向右、向前和向左、向后和向右、向后和向左移动。这个相机系统的优点是向前移动是相对于我们观察的方向的。所以,如果我们看一个特定的立方体,然后按向前键,它就会移动到我们的立方体方向。如果我们试图穿过立方体,我们会穿过它,并且可以看到纹理的逆面。我们可以穿过它的原因很简单,那就是没有碰撞检测。

摘要

在本章中,我们学习了如何应用变换,如旋转到我们的形状上,并学会了如何绘制一个立方体并将其纹理化。然后,我们探讨了投影的概念,包括透视和正交投影,并在我们的游戏世界中实现了这些概念。

在下一章中,我们将讨论光照、其效果以及我们在 OpenGL 中拥有的光源。

第四章:光照、材质和光照贴图的效果

在上一章中,我们讨论了如何将变换和投影应用于对象。我们还创建了多个立方体和一个Camera类,以便清晰地查看并导航这些对象。在本章中,我们将探讨光照。首先,我们将讨论与我们的对象和光源相关的颜色基础知识。我们还将讨论创建顶点着色器和片段着色器,就像我们在上一章为对象箱所做的那样。我们将为实际的光源,如灯具,创建着色器。你还将学习如何将材质应用于你的对象立方体,并观察光照对这些材质的影响。

在本章中,我们将涵盖以下主题:

  • 光照中颜色的基础知识以及光照对对象的影响

  • 光对某类材料的影响

  • 探索光照贴图以实现不同材料上光照的真实世界效果

光……摄像机……开拍!!

你可以参考 GitHub 上Chapter04文件夹中的所有本章代码文件。GitHub 链接可以在书的序言中找到。

添加对象和光源

在本节中,我们将讨论如何将颜色应用于你的立方体对象。我们还将学习如何为光和灯具等光源创建着色器文件。然后我们将学习如何将立方体和光源添加到我们的游戏世界中。

因此,让我们首先创建光和灯具的新着色器文件。

创建光照和灯具着色器文件

在这里,我们将学习如何为光源和灯具创建着色器文件,并探索将进入顶点着色器和片段着色器的代码。执行以下步骤以学习如何创建这些着色器文件:

  1. 首先,将上一章中的core.vscore.frag重命名为lighting.vslighting.core

  2. 现在,让我们开始修改这些新命名的文件的代码。首先,我们将修改lighting.vs。我们在这里要做的就是移除纹理坐标,因为我们在这里不会渲染纹理,我们还将移除out texture变量。查看以下代码以了解对光照顶点着色器所做的更改:

#version 330 core
layout (location = 0) in vec3 position;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
 gl_Position = projection * view * model * vec4(position, 1.0f);
}
  1. 接下来,我们将转到lighting.frag并按照以下代码进行修改:
#version 330 core
out vec4 color;

uniform vec3 objectColor;
uniform vec3 lightColor;

void main()
{
    color = vec4(lightColor * objectColor, 1.0f);
}

在前面的代码中,我们添加的变量objectColor将包含对象本身的颜色,在我们的例子中是立方体。

  1. 保存这两个文件,现在我们将为我们的灯具创建着色器文件。

为灯具创建着色器文件

查看以下步骤,了解如何创建灯具着色器:

  1. 复制我们在前面步骤中更新的文件,并将它们重命名为lamp.vslamp.frag,我们还需要在这些新文件中修改一些代码以创建我们的光源。

  2. 我们不会对lamp.vs进行任何修改,因为我们需要当前的更新代码。

  3. 我们需要对lamp.frag进行一些修改,所以请查看以下代码中突出显示的术语:

#version 330 core
out vec4 color;

void main()
{
    color = vec4(1.0f); // Set all 4 vector values to 1.0f
}

我们将值传递给vec4作为1.0f的原因是,所有矢量值都应该设置为1.0f,这是高强度的红色、绿色、蓝色和 alpha。所以,它将完全开启。如果你有全红、全绿和全蓝,你得到白色。因此,我们的灯将发出白光。

你可能在高中做过一个实验,将彩虹的所有不同颜色放在一个圆圈上,如果你足够快地旋转它,组合的颜色看起来是白色的。这是一个相当酷的实验,你很可能在家里也能做到。总的来说,这是一件有趣的事情,值得尝试。

因此,现在我们已经为光照和灯设置了所有着色器文件,我们将继续在main.cpp文件中的主代码,向游戏世界中添加一个对象和光源。

修改主代码以实现立方体和光源

现在我们已经为我们的项目创建了新的着色器文件,接下来我们将着手于我们的主代码,并在游戏世界中添加一个彩色立方体和光源。在本节中,我们还将查看如何在代码中引用我们新创建的着色器文件。在这里,我们将对上一章的代码进行修改。执行以下步骤以了解对代码所做的更改:

  1. 在我们的int main()之前,我们将首先添加glm::vec3 lightPos();。所以,这将是我们世界中光源的位置。我们将向lightpos()函数传递以下坐标:1.2f1.0f2.0f。这样工作的方式是,你有一个从特定位置发出的光源,例如,如果你加载一个灯泡作为光源并将其放置在定义的位置。灯泡本身是我们世界中的光源。

  2. 接下来,我们将进入定义了我们的着色器的部分。随着我们向项目中添加新的着色器,我们将在代码中引用它们。

  3. 现在我们已经创建了一些着色器,我们将复制我们代码中现有的Shader ourShader( );函数,并将其重命名为lightingShaderlampShader。显然,我们需要更新提到的路径,以便引用我们的光照和灯着色器文件。请查看以下突出显示的代码:

  Shader lightingShader( "res/shaders/lighting.vs",
  "res/shaders/lighting.frag" );
  Shader lampShader( "res/shaders/lamp.vs", "res/shaders/lamp.frag" );
  1. 接下来,对于顶点,我们将做的是移除我们数组中存在的所有纹理坐标。因为我们在这个代码中不会渲染任何纹理,我们只需要 x、y 和 z 坐标来描述我们的立方体。你可以参考位于Chapter04文件夹中colours文件夹内的main.cpp文件中更新的顶点。

  2. 然后,我们将移除cubePositions []数组,因为我们将在我们的世界中渲染单个立方体。这将使我们更容易理解光线对我们对象的影响。

  3. 接下来,在代码中,我们定义了顶点缓冲对象和顶点数组对象的地方,我们将对其进行以下修改:

 // First, set the container's VAO (and VBO)
    GLuint VBO, boxVAO;
    glGenVertexArrays( 1, &boxVAO );
    glGenBuffers( 1, &VBO );

我们进行这次修改的原因是我们将有一个要重复使用的顶点缓冲对象,但对于顶点数组对象,每个单独的着色器和盒子将会有一个不同的对象。

  1. 现在,在位置属性中,我们将更新5 * sizeof()3 * size of(),因为我们不再在顶点数组中有一行五条信息,这些信息是 3 个坐标:xyz以及两个纹理坐标。现在,由于我们不再使用纹理坐标,数组中只有xyz坐标。

  2. 接下来,我们将删除纹理坐标属性,因为我们不再在代码中加载纹理。

  3. 接下来,我们将复制顶点定义代码、顶点绑定代码和位置属性代码,并将其粘贴在位置属性代码下方。在这些复制的代码行中,我们将进行以下突出显示的更改,以将光顶点数组对象添加到我们的主代码中:

// Then, we set the light's VAO (VBO stays the same. After all, the vertices are the same for the light object (also a 3D cube))

GLuint lightVAO;
glGenVertexArrays( 1, &lightVAO );
glBindVertexArray( lightVAO );

 // We only need to bind to the VBO (to link it with glVertexAttribPointer), no need to fill it; the VBO's data already contains all we need.

 glBindBuffer( GL_ARRAY_BUFFER, VBO );

// Set the vertex attributes (only position data for the lamp)

glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof( GLfloat ), ( GLvoid * )0 );
glEnableVertexAttribArray( 0 );
glBindVertexArray( 0 );
  1. 下一步,我们将删除整个创建和加载纹理的代码。

  2. 在上一章中,我们在循环内添加了投影矩阵,并且每次循环运行时都会声明。现在,由于我们使用 GetZoom 获取视场,我们不能在循环中放置投影矩阵。因此,我们最好将投影矩阵代码从循环中提取出来,并将其粘贴在 while 循环的开始处。

对 while 循环的修改

在 while 循环内部,我们需要更改一些内容,让我们看看:

  1. 首先,我们将更改背景颜色,使其更暗,这样我们尝试实现的照明将对我们的对象有更大的影响。因此,我们将对glClearColor()函数进行以下修改:
glClearColor( 0.1f, 0.1f, 0.1f, 1.0f  );
  1. 接下来,我们将从定义我们的绑定纹理代码的位置开始,删除所有代码,直到定义我们的绘制容器代码的位置,并添加新的代码。

  2. 因此,我们将使用相应的着色器设置统一对象并绘制对象的代码。首先,我们将添加lightingShader.Use();,因为我们正在处理盒子的照明着色器。然后我们将创建一个GLuint变量objectColorLoc,并将函数glGetUniformLocation()的值分配给它,该函数将包含参数,如lightingShader.Program"objectColor"

如同往常,如果您想了解更多关于我们讨论的细节,可以查看learnopengl.comopen.gl。那里有一些写得很好的教程,并且有图像伴随这些教程,这是在这些章节之上学习的好方法。

  1. 接下来,我们将复制前面的一行代码,并对它进行以下高亮修改:
lightingShader.Use( );
GLint objectColorLoc = glGetUniformLocation( lightingShader.Program, "objectColor" );
GLint lightColorLoc  = glGetUniformLocation( lightingShader.Program, "lightColor" );
  1. 然后,我们将添加函数glUniform3f(),并为其设置对象颜色位置。因此,我们将参数传递为objectColorLoc1.0f0.5f0.31f

    这些显然只是我们确定的任意值,并且它们实际上工作得很好。显然,在你未来的项目中,当你不遵循本章内容时,你可以尝试对这些值进行实验。我们只需复制前面的一行代码,并对它进行以下高亮修改:

glUniform3f( objectColorLoc, 1.0f, 0.5f, 0.31f );
glUniform3f( lightColorLoc, 1.0f, 0.5f, 1.0f );
  1. 现在我们将创建一个相机变换。因此,我们将视图矩阵glm::mat4 view;添加到我们的代码中,然后输入view = camera.GetViewMatrix

  2. 接下来,我们将获取模型、视图和投影矩阵的统一位置。因此,我们将输入GLint modelLoc = glGetUniformLocation();。在那里,我们将传递lightingShader.Programmodel

    我们将复制前面的一小部分代码,并对它进行以下高亮修改,如下所示:

// Create camera transformations
glm::mat4 view;
view = camera.GetViewMatrix( );

// Get the uniform locations
GLint modelLoc = glGetUniformLocation( lightingShader.Program,"model");
GLint viewLoc = glGetUniformLocation( lightingShader.Program,"view");
GLint projLoc = glGetUniformLocation( lightingShader.Program, "projection" );
  1. 现在我们将传递矩阵到着色器中。所以现在,我们只需要添加glUniformMatrix4fv();。我们将向这个函数传递viewLoc1GL_FALSEglm::value_ptr(),而对于值指针函数,你只需指定我们的 4x4 视图矩阵。

  2. 复制前面的一行代码,因为我们还需要对投影矩阵做同样的操作。查看以下代码及其中的高亮部分:

// Pass the matrices to the shader
glUniformMatrix4fv( viewLoc, 1, GL_FALSE, glm::value_ptr( view ) );
glUniformMatrix4fv( projLoc, 1, GL_FALSE, glm::value_ptr( projection ) );
  1. 现在我们将使用容器的顶点属性来绘制容器,这是简单的事情,我们已经在之前的章节中讨论过。如果你想复习,请随意。查看以下代码:
// Draw the container (using container's vertex attributes)
glBindVertexArray( boxVAO );
glm::mat4 model;
glUniformMatrix4fv( modelLoc, 1, GL_FALSE, glm::value_ptr( model ) );
glDrawArrays( GL_TRIANGLES, 0, 36 );
glBindVertexArray( 0 );

在前面的代码中,我们输入了 36。这样做的原因是,总共有 36 个顶点,每边 6 个,一个立方体有 6 个面,所以我们在glDrawArrays()函数中传递了 36。

  1. 接下来,我们将复制我们在上一步中描述的代码,并将其粘贴在前面代码的下方。然后,我们将对灯的着色器执行以下高亮修改:
// Also draw the lamp object, again binding the appropriate shader
lampShader.Use( );

// Get location objects for the matrices on the lamp shader (these could be different on a different shader)
modelLoc = glGetUniformLocation( lampShader.Program, "model" );
viewLoc = glGetUniformLocation( lampShader.Program, "view" );
projLoc = glGetUniformLocation( lampShader.Program, "projection" );

// Set matrices
glUniformMatrix4fv( viewLoc, 1, GL_FALSE, glm::value_ptr( view ) );
glUniformMatrix4fv( projLoc, 1, GL_FALSE, glm::value_ptr( projection ) );
model = glm::mat4( );
model = glm::translate( model, lightPos );
model = glm::scale( model, glm::vec3( 0.2f ) ); // Make it a smaller cube
glUniformMatrix4fv( modelLoc, 1, GL_FALSE, glm::value_ptr( model ) );

// Draw the light object (using light's vertex attributes)
glBindVertexArray( lightVAO );
glDrawArrays( GL_TRIANGLES, 0, 36 );
glBindVertexArray( 0 );

在前面的代码中,在glm::vec3( 0.2f )中,我们添加了 0.2f,因为我们想要在每个轴向上以0.2f的比例缩放它。我们这样做的原因是我们不希望我们的光源,我们的灯,与我们的立方体大小相同。这只是我们感知世界的方式。一般来说,灯泡比我们感知的房间里的大多数东西都要小。

  1. 现在,我们唯一需要做的就是更新glDeleteVertexArrays()。我们将按以下方式更新:
glDeleteVertexArrays( 1, &boxVAO );
glDeleteVertexArrays( 1, &lightVAO );
glDeleteBuffers( 1, &VBO );

我们现在可以运行并查看我们的杰作了。检查你窗口上的输出:

图片

我们有一个类似红色的盒子,并且我们有我们的白色光源。它看起来并没有真正发出任何光,但这只是基础。但这是一个很好的前奏,为未来的章节和创建真正酷炫的光照效果的未来。

在下一节中,我们将探讨一些非常酷的基本光照,这将使效果看起来好得多。因此,我们建议你在代码中额外完成一个任务:找出你可以改变我们的物体和发射光源颜色的地方。这就是现代 OpenGL 中光照的基本颜色所涉及的全部内容。

照明物体

在本节中,我们将探讨光照的基础知识,因为到目前为止,如果你查看我们上一节的结果,该节仅讨论了颜色,我们所得到的是立方体和光源。目前,立方体输出的整个颜色看起来非常均匀。它几乎不像一个立方体,一个六边形的形状,而且光照看起来也不太真实。因为在现实中,在我们的例子中,光源并没有向我们的立方体发射光线,立方体上也没有光和影的效果。所以,在本节中,我们将讨论光照的基础知识及其对立方体的影响。我们将专注于改进光照系统,以便我们可以获得更真实的效果。

那么,让我们开始吧。像往常一样,我们将开始修改我们的着色器文件。

更新着色器

查看以下步骤以了解对着色器所做的更改:

  1. 我们不会在灯具着色器文件中进行任何更改,因为我们对于实际发出的光线感到满意。

  2. 接下来,你想要做的是进入你的光照顶点着色器,本质上改变我们的立方体感知光线的方式,这将使物体看起来不同。所以,如果你进入我们的光照顶点着色器,这不需要很多更改。看看以下突出显示的代码:

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;

out vec3 Normal;
out vec3 FragPos;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position = projection * view *  model * vec4(position,
    1.0f);
    FragPos = vec3(model * vec4(position, 1.0f));
    Normal = mat3(transpose(inverse(model))) * normal;
}

什么是法线?

首先,让我们了解什么是法线。法线基本上是一个方向,它们垂直于特定的表面:

图片

所以,正如你可以在前面的图中看到的,法线与表面成 90 度角,这在计算光照时很有用,因为它决定了光线如何从表面反射,表面如何对光线做出反应,因此,它看起来是某种特定的样子。一种强大的技术是改变法线的方向,你可以在后面的章节中了解到这一点)并允许你改变光线对它的反应方式,这使得物体看起来不同。我们甚至可以做到,比如说,一个平面物体,或者一个相对平面的物体,具有非常低的多边形计数,通过改变法线,我们可以给它添加深度错觉。这就是为什么当你玩游戏时,有时你会看到某种物体,尤其是在墙上时,它相对平坦,但它看起来有点深度。如果你离它很远,它看起来有深度。当你靠近,尤其是当你从某个角度观察它时,它不再有深度,或者深度非常小。这就是这个系统的局限性。显然,如果你想克服这一点,你需要使用某种类型的细分技术,它实际上具有真实的几何形状。这显然在处理能力方面要昂贵得多。这在处理能力方面要便宜得多,所以在游戏行业中,这真的是首选,因为你不仅仅是在画一个简单的形状。你是在画一大堆多边形,这种技术将允许你保留一些处理能力。

更新 lighting.frag 着色器

让我们按照以下步骤进行:

  1. 现在我们已经完成了这个步骤,接下来我们进入光照片段着色器,并对它进行以下高亮显示的修改:
#version 330 core
out vec4 color;
in vec3 FragPos;
in vec3 Normal;

在前面的代码中,我们添加了 FragPos,因为那些是我们从顶点着色器发送出去的片段位置。

  1. 然后我们创建了如下所示的统一向量变量:
uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform vec3 objectColor;

在前面的代码中,我们添加了 lightPos,因为我们需要一个表示光位置的向量,因为我们正在考虑光的位置,光照将根据你所看的表面部分而变化。

在我们的主函数中,我们将使用三种光照技术作为着色技术,它们将是环境光、漫反射光和镜面光。我们将详细讨论它们,并了解如何在我们的代码中定义它们。

环境光照

第一种,即环境光照,就像场景中的普通光照一样。它不像太阳光,而是一直在房间中反弹的普通光。它没有特定的起源、位置或方向。因此,它可以为你的物体提供一些基本的颜色,一些基本属性。在此基础上,你还可以添加漫反射光照和镜面光照,使物体成为一个独特且有趣的物体,使其看起来更接近现实世界。

我们将首先添加float ambientStrength,并将其设置为0.1f。您可以随意更改该值并观察结果,同时也要了解变量的限制。然后我们将添加vec3用于环境照明。该变量将等于ambientStrength * lightColor

void main()
{
 // Ambient
 float ambientStrength = 0.1f;
 vec3 ambient = ambientStrength * lightColor;

漫反射照明

现在我们将进行漫反射照明。漫反射照明考虑了光的方向和法线。例如,想象我们的立方体,靠近光源的角会比远离光源的角更亮。本质上,漫反射照明是什么?它调整了实际的位置和角度。这也与角度有关,所以如果你有一个 90 度的光源,它发出的光会比 5 度的光源多,90 度的光源会更亮。这就是漫反射的本质。你通常不会只有环境、漫反射或镜面反射照明。你会有这三种光结合在一起,以不同的强度和强度,这允许你创建一个称为组合照明的真实效果,这种效果通常被称为 Phong 着色。你可以在互联网上了解它。看看以下关于漫反射照明的代码:

// Diffuse
 vec3 norm = normalize(Normal);
 vec3 lightDir = normalize(lightPos - FragPos);
 float diff = max(dot(norm, lightDir), 0.0);
 vec3 diffuse = diff * lightColor;

在前面的代码中,我们添加了lightPos - FragPos。这两个值之间的差异将告诉你光线指向的方向。在代码的末尾,我们总是要考虑lightColor,因为最终,如果我们有一种白光照射在某个物体上,我们不想它变成蓝色或红色。它必须保持光的本色。显然,这会根据实际应用的材料和该物体的性质而变化,但实际的光本身应该是光的颜色。

现在我们已经完成了漫反射照明,我们可以继续进行镜面反射照明。

镜面反射照明

要谈论镜面反射照明,想象一个斯诺克球或台球,例如,它上面有一个小圆圈的光照在上面,它总是那个小亮光在物体上。让我们看看以下代码:

     // Specular 
     float specularStrength = 0.5f;
     vec3 viewDir = normalize(viewPos - FragPos);
     vec3 reflectDir = reflect(-lightDir, norm);
     float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
     vec3 specular = specularStrength * spec * lightColor;

     vec3 result = (ambient + diffuse + specular) * objectColor;
     color = vec4(result, 1.0f);
}

现在我们已经更新了着色器文件,我们需要在Camera.h文件中进行细微的更改。

Camera.h文件中进行了细微的更改

Camera.h文件中,我们没有获取相机位置的方法。所以,我们将做的是,在glfloat GetZoom()下面添加glm::vec3 GetPosition ()方法,它将简单地返回位置。看看以下代码:

glm::vec3 GetPosition ()
{
    return this ->position
}

修改主代码

现在我们只需要去我们的main.cpp文件,并修改我们的代码。看看以下步骤:

  1. 在那里,我们需要修改的第一件事是顶点数组。目前,我们为每个顶点都有 x、y 和 z 值,我们还需要包含法线。你可以参考Basic Lighting文件夹中main.cpp文件中更新的顶点。为了参考,只需看看立方体一面的顶点:
     //Position             //Normal    
     -0.5f, -0.5f, -0.5f,   0.0f, 0.0f, -1.0f,
      0.5f, -0.5f, -0.5f,   0.0f, 0.0f, -1.0f,
      0.5f, 0.5f, -0.5f,    0.0f, 0.0f, -1.0f,
      0.5f, 0.5f, -0.5f,    0.0f, 0.0f, -1.0f,
     -0.5f, 0.5f, -0.5f,    0.0f, 0.0f, -1.0f,
     -0.5f, -0.5f, -0.5f,   0.0f, 0.0f, -1.0f,

在前面数组中我们得到的三个额外值是法线的方向。每个面的法线都将保持不变。

额外任务:试着找出这些法线中的每一个适用于哪个面,一旦你渲染了它,方向是什么。修改它们,看看会发生什么。更改一些这些值,但保留一些不变。看看会发生什么。

  1. 一旦我们整理好所有这些,我们只需要更改我们在定义我们的VBOboxVAO时的一些地方。无论我们提到boxVAO的地方,我们都会将其替换为containerVAO
 GLuint VBO, containerVAO;
 glGenVertexArrays( 1, &containerVAO );
 glGenBuffers( 1, &VBO );

 glBindBuffer( GL_ARRAY_BUFFER, VBO );
 glBufferData( GL_ARRAY_BUFFER, sizeof( vertices ), vertices, GL_STATIC_DRAW );

 glBindVertexArray( containerVAO );
  1. 在位置属性中,我们将进行以下突出显示的更改,并且类似地,我们还将创建我们的法线属性。看看以下突出显示的代码:
// Position attribute
 glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof( GLfloat ), ( GLvoid * )0 );
 glEnableVertexAttribArray( 0 );

 // Normal attribute
 glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof( GLfloat ), ( GLvoid * )( 3 * sizeof( GLfloat ) ) );
 glEnableVertexAttribArray( 1 );
 glBindVertexArray( 0 );   

由于顶点数组的每一行都有六个不同的值,这就是我们在代码中更新 6 的原因。你还需要在lightVAO的位置属性中更新类似的6值。

  1. 在代码中定义我们的光照着色器时,我们使用对象颜色位置和光照颜色位置。现在,我们还需要做的是添加光照位置位置并将其分配给我们的着色器程序,并添加视图位置位置。所以,我们将复制整个GLint lightColorLoc代码两次,并添加以下更新以添加光照位置位置和视图位置位置。我们还需要为gluniform3f();做类似的事情。看看以下突出显示的代码:
lightingShader.Use( );
 GLint objectColorLoc = glGetUniformLocation( lightingShader.Program, "objectColor" );
 GLint lightColorLoc = glGetUniformLocation( lightingShader.Program, "lightColor" );
 GLint lightPosLoc = glGetUniformLocation( lightingShader.Program, "lightPos" );
 GLint viewPosLoc = glGetUniformLocation( lightingShader.Program, "viewPos" );
 glUniform3f( objectColorLoc, 1.0f, 0.5f, 0.31f );
 glUniform3f( lightColorLoc, 1.0f, 1.0f, 1.0f );
 glUniform3f( lightPosLoc, lightPos.x, lightPos.y, lightPos.z );
 glUniform3f( viewPosLoc, camera.GetPosition( ).x, camera.GetPosition( ).y, camera.GetPosition( ).z )

一旦我们更新了所有这些内容,我们就会保留其余的代码不变。因为我们不需要对其进行任何修改。

我们实际上应该准备好运行代码了,但记住上一节中的样子。你屏幕上得到的输出应该看起来有些类似于此:

如你可能在前面图像中注意到的,左上角,靠近左上角,看起来比左下角亮一些,这是因为那里是光源所在。在其他立方体的侧面和顶部上这一点非常明显。只需比较顶部与其他侧面。这些侧面较暗,因为它们的表面上几乎没有光照。你可以在以下屏幕截图中查看:

只是,它比以前更真实。你也会注意到,当你移动到它上面时,你将看到我们立方体上的美丽动态着色。

所以我们现在要做的就是进入光照片段着色器,将specularStrength改为2.0f。只需修改这些内容,并观察将产生的输出,看看下面的截图:

你已经可以看到代码中值的变化对光强度的影响了。看看那个光泽。那是在台球上得到的那种光泽。尝试调整这个值。如果你将ambientStrength的值改为0.5f,你会看到一个更亮的立方体:

现在看起来几乎像是一种均匀的颜色了,但看起来也很酷。尝试通过更改着色器文件和主代码中的值来实验。

我们最后要尝试的事情是移动光源的位置,这样你就可以看到它对我们物体的影响。所以,在我们的主代码中,while 循环的开始处,我们将添加lightPos.x -= 0.01f,并且我们也将对 z 位置做同样的操作:

lightPos.x -= 0.01f
lightPos.z -= 0.01f

我们不会修改 y 位置,因为我们只希望光源沿着地平线移动。你也可以让它沿着 y 轴移动;我们建议你这样做。尝试围绕它画一个圆。保存更改并运行代码。你会在屏幕上观察到以下输出:

如你所见,我们有一个光源,并且随着它越来越远,顶部变得越来越暗,因为它处于一个非常隐蔽的角度。随着它越来越远,你会观察到顶部变得越来越暗:

作为额外任务,我们建议你尝试将光源移动到尽可能远的位置,并观察效果。如果能让它围绕物体本身旋转,并尝试添加多个光源,那将会产生一个很棒的效果。

材料信息

我们将在本节中讨论材质。因此,让我们首先了解什么是材质以及为什么你会使用它。在现实世界中,你有一些由不同材质制成的物体。有些物体是由金属、木材、塑料和其他材料制成的。因此,这些材质、这些物体根据它们的材质对光有不同的反应。一般来说,由金属制成的物体会比由木材制成的物体更亮。木材通常不发光。显然,如果你在上面涂上某种清漆,它就会发光,但再次强调,那将是物体上除木材之外的一个额外层。它将是某种比木材更多,并施加在其上以提供某种光泽的东西。你有光泽材料,还有一些更哑光的材料。简单来说,OpenGL 中的材质将允许我们创建对光有不同的反应的物体,从而在我们的游戏或应用程序中创建更真实和多样的效果。这就是你为什么要使用它的原因,以获得你试图创建的物体的更多样化和真实的表现。

那么,让我们开始吧...

几乎没有先决条件。本节使用上一节中的源代码,该节介绍了基本光照。如果你没有那个代码,请随意查看前言中的 GitHub 链接以获取代码文件。我们还想提到的是,这些章节基于 learnopengl.com 和 open.gl 的工作。他们使用精彩的插图来解释我们在做什么。你也可以随意查看他们的页面,因为那里有大量信息。他们深入探讨了我们在所有章节中讨论的代码。所以,这是一种增强你已知知识的好方法。

在本节中,我们将使立方体循环通过各种不同的材质。我们将首先更新着色器文件。

更新材质的着色器文件

看看以下步骤

  1. 我们不会对顶点着色器进行任何修改,即lighting.vs

  2. 接下来,我们将转向光照片段着色器,在这里,在最上面,我们将创建一个名为Material的数据类型struct。在那里,我们将有三个变量vec3,它们将是ambientdiffusespecular,以及一个shininess浮点数,这样我们就可以轻松地改变镜面光的强度。请看以下代码:

struct Material
{
 vec3 ambient;
 vec3 diffuse;
 vec3 specular;
 float shininess;
};
  1. 接下来,我们将为光创建一个数据类型struct。这将有一个位置vec3,还将有三个额外的vec3用于环境光、漫反射和镜面光:
struct Light
{
 vec3 position;

 vec3 ambient;
 vec3 diffuse;
 vec3 specular;
};

如果你想知道更多关于环境光、漫反射和镜面光是什么的信息,请随意查看前面的章节。或者,你也可以去 learnopengl.com 和 open.gl。这些网站提供了大量信息。

  1. 然后,我们将去掉lightPos,因为我们已经在前面的结构体中有它。我们不需要objectColor,也不需要lightColor,因为我们再次在结构体中拥有它们。然后我们将添加uniform Material materialuniform Light light
uniform vec3 viewPos;
uniform Material material;
uniform Light light;
  1. 现在,在void main()中,我们将做什么?对于环境光,我们将去掉ambientStrength,并将其修改为Vec3 ambient = light.ambient * material.ambient

  2. 对于漫反射光,vec3 lightDir,我们需要稍作修改。看看下面高亮显示的术语:

vec3 lightDir = normalize(light.position - FragPos);
  1. 对于最终的漫反射计算,我们只需要稍作修改。我们需要添加light.diffuse乘以diff,这是在这里用浮点变量计算的,以及diff乘以material.diffuse。我们在我们的光照、着色中的每一个部分都考虑了材质,因为这很重要。这就是我们的实际物体或物体的一部分将呈现的样子,因为我们应用了不同的材质。

  2. 在镜面反射中,我们可以去掉specularStrength,因为我们已经在前面的代码中有它。然后我们将更新float spec用于光泽度;我们将添加material.shininess。对于vec3 specular,我们稍作修改。我们将添加light.specular * (spec * material.specular)。对于结果,我们将按照以下代码中的高亮显示进行修改:

void main()
{
 // Ambient
 vec3 ambient = light.ambient * material.ambient;

 // Diffuse
 vec3 norm = normalize(Normal);
 vec3 lightDir = normalize(light.position - FragPos);
 float diff = max(dot(norm, lightDir), 0.0);
 vec3 diffuse = light.diffuse * (diff * material.diffuse);

 // Specular
 vec3 viewDir = normalize(viewPos - FragPos);
 vec3 reflectDir = reflect(-lightDir, norm);
 float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
 vec3 specular = light.specular * (spec * material.specular);

 vec3 result = ambient + diffuse + specular;
 color = vec4(result, 1.0f);
}

保存这些更改,现在我们已经完成了片段着色器的更新。

修改主代码以添加材质到我们的物体

按照以下步骤添加材质到我们的物体并观察光照对其的影响:

  1. 如果你打开main.cpp,你实际上想要做的是直接进入 while 循环,因为循环之外的所有内容都应该保持原样,不需要任何修改。

  2. 因此,在lightingShader.Use();下面的代码行中,我们将去掉objectColorLoclightColorLoc。我们想要lightPosLoc,但其中的一个参数需要更改为light.position,因为我们已经在片段着色器中更新了它。

  3. gluniform3f中,你可以去掉objectColorLoclightColorLoc

  4. 现在我们还需要设置光源的属性。为此,我们将添加glm::vec3 lightColor;,并添加lightColor.r = sin();,对于sin(),这是我们将在框架特定的代码中处理的地方。我们只需传递glfwGetTime(),这会获取自 GLFW 初始化以来经过的时间量。我们将将其乘以2.0f。我们将复制这一行代码,将其粘贴在下面,并对其进行以下高亮显示的修改:

// Set lights properties
 glm::vec3 lightColor;
 lightColor.r = sin( glfwGetTime( ) * 2.0f );
 lightColor.g = sin( glfwGetTime( ) * 0.7f );
 lightColor.b = sin( glfwGetTime( ) * 1.3f );
  1. 因此,现在我们已经设置了光源的属性,我们需要实际设置漫反射颜色和环境颜色。所以,我们将添加glm::vec3 diffuseColor = lightColor * glm::vec3();。对于ver3(),我们只需提供一个值为0.5f的值。这将减少漫反射颜色的影响。

  2. 接下来,我们将添加 glm::vec3 ambientColor = diffuseColor * glm::vec3(); 并且在这里,vec3 将会是 0.2f,因为这只是一个低强度的。所以尝试通过修改这些值进行一些实验,看看你得到什么结果。

  3. 然后,我们需要添加的是 glUniform3f() 函数,我们将传递 glGetUniformLocation() 函数,对于这个函数,我们现在将指定 lightingShader.Program"light.ambient"。然后我们将传递 ambientColor.r,同样地,我们将传递 ambientColor.gambientColor.b

  4. 接下来,我们将重复之前描述的代码,并对其进行以下突出显示的更改:

glm::vec3 diffuseColor = lightColor * glm::vec3( 0.5f ); // Decrease the influence
glm::vec3 ambientColor = diffuseColor * glm::vec3( 0.2f ); // Low influence
glUniform3f( glGetUniformLocation( lightingShader.Program, "light.ambient" ), ambientColor.r, ambientColor.g, ambientColor.b );
glUniform3f( glGetUniformLocation( lightingShader.Program, "light.diffuse" ), diffuseColor.r, diffuseColor.g, diffuseColor.b);
glUniform3f( glGetUniformLocation( lightingShader.Program, "light.specular" ), 1.0f, 1.0f, 1.0f );
  1. 现在我们将设置材质属性,为此,我们将进行环境光、漫反射、镜面反射和光泽度的设置。所以,你想要添加 glUniform3f(); 并将 glGetUniformLocation() 传递给它,然后我们将指定 lightingShader.Program 和选定的 material.ambient。然后我们将传递一些值。我们将只放入一些显式的值,如 1.0f0.5f0.31f

  2. 只需将前面的代码复制粘贴几次,并按照以下代码中突出显示的进行修改:

// Set material properties
 glUniform3f( glGetUniformLocation( lightingShader.Program, "material.ambient" ), 1.0f, 0.5f, 0.31f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "material.diffuse"), 1.0f, 0.5f, 0.31f );
 glUniform3f(glGetUniformLocation( lightingShader.Program, "material.specular" ), 0.5f, 0.5f, 0.5f ); // Specular doesn't have full effect on this object's material
 glUniform1f(glGetUniformLocation( lightingShader.Program, "material.shininess" ), 32.0f );

在前面的代码中,当我们定义光泽度时,我们在 glUniform1f() 中只添加了一个浮点值,因为光泽度不是一个向量或数组,或其他类似的东西。

现在我们已经准备好了代码。所以让我们运行这段代码,并检查我们在屏幕上得到的输出。你可能在你的窗口中得到一个类似的结果:

你会在屏幕上观察到颜色变化的立方体。它看起来正好是我们想要的,非常漂亮。尝试移动光源,并观察立方体表面的阴影效果:

这样就可以在 OpenGL 中使用材质为我们的对象添加效果。

光照贴图

让我们在本节中讨论光照贴图。但首先,让我们尝试理解什么是光照贴图。好吧,在现实世界中,如果你有一个,比如说,部分是金属部分是木材的箱子,类似于以下截图:

与金属部分相比,它的木质部分对光线的反应将不同。这听起来可能很明显,但 OpenGL(或任何其他类型的 3D 图形 API)没有木材、金属、塑料或其他任何概念,因此我们需要在程序中实现它。我们需要使用其他技术来帮助在视觉上说明这一点,因为木材应该比箱子的金属部分更不反光。如果你搜索光照贴图,你会得到很多信息。你可以在各种工具中创建它们。你可以使用 Maya 或 Photoshop 来创建它们。所以,不再拖延,让我们开始编码。

修改着色器文件

按照以下步骤进行操作:

  1. 因此,您需要做的是转到光照顶点着色器。我们需要对其进行一些修改,因为我们移除了应用纹理的能力,而且在之前的几个部分中我们没有这样做。所以,我们需要再次添加它。查看以下修改以了解代码的更改:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords; 
out vec3 Normal;
out vec3 FragPos;
out vec2 TexCoords; 
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() 
{
     gl_Position = projection * view * model * vec4(position,
     1.0f);
     FragPos = vec3(model * vec4(position, 1.0f));
     Normal = mat3(transpose(inverse(model))) * normal;
     TexCoords = texCoords;
}

保存这些更改,现在我们需要对片段着色器进行修改。

  1. lighting. frag 中,我们将对代码进行以下修改:我们将从结构体 material 中移除所有代码并添加新的代码。我们将添加 sampler2D diffusesampler2D specular,这就是那个带有亮部和暗部的镜面光图。然后我们将添加 float shininess。光泽度始终很重要:
struct Material
{
     sampler2D diffuse;
     sampler2D specular;
     float shininess;
};  
  1. 对于输入,我们还需要纹理坐标。因此,我们将添加 in vec2 TexCoords:
struct Light
{
     vec3 position;

     vec3 ambient;
     vec3 diffuse;
     vec3 specular;
};

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords; out vec4 color;

uniform vec3 viewPos;
uniform Material material;
uniform Light light;
  1. 现在,void main () 中的 vec3 ambient 需要稍作修改,因为我们正在使用纹理。所以,我们需要做的就是移除 material.ambient 并添加 vec3(),在其中,我们想要指定 texture()。在这个方法中,我们将传递 material.diffuseTexCoords

  2. 现在,对于最终计算中的漫反射光,(diff * material.diffuse) 需要稍作修改,因为我们现在正在使用纹理。所以,我们将移除前面的项并添加 light.diffuse * diff * vec3()。我们将传递 texture(),并在其中指定 material.diffuseTexCoords。其余的都是好的。现在让我们转到镜面光。在最后一步,我们只需要以类似的方式修改它,因为我们现在正在使用纹理。查看以下代码以了解描述:

void main()
{
     // Ambient
     vec3 ambient = light.ambient * vec3(texture(material.diffuse,
    TexCoords));

     // Diffuse
     vec3 norm = normalize(Normal);
     vec3 lightDir = normalize(light.position - FragPos);
     float diff = max(dot(norm, lightDir), 0.0);
     vec3 diffuse = light.diffuse * diff *
     vec3(texture(material.diffuse, TexCoords));

     // Specular
     vec3 viewDir = normalize(viewPos - FragPos);
     vec3 reflectDir = reflect(-lightDir, norm);
     float spec = pow(max(dot(viewDir, reflectDir), 0.0),
     material.shininess);
     vec3 specular = light.specular * spec * 
     vec3(texture(material.specular, TexCoords));

     color = vec4(ambient + diffuse + specular, 1.0f);
}

因此,我们现在已经完成了着色器的所有工作。我们可以实际上转到 main.cpp

实现光照图的代码更改

让我们按照以下步骤实现光照图:

  1. 在主代码中,您需要更改的第一件事是顶点,因为我们目前有位置和法线。我们还需要指定纹理坐标,因为我们现在正在使用纹理。您可以参考 Lighting Maps 文件夹中的 main.cpp 文件中的更新后的顶点。将更新后的顶点复制并粘贴到我们的主代码中。

  2. 接下来,转到我们绑定顶点和创建顶点指针的位置。由于我们向代码中添加了纹理系统,我们需要稍微修改顶点指针和法线属性。由于我们的顶点数组中有八个信息,我们将用 8 替换 6。我们还需要复制法线属性代码,粘贴它,并修改它以用于纹理属性。查看以下代码以了解所做的修改:

 glBindVertexArray( containerVAO );
 glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(
 GLfloat ), ( GLvoid * )0 );
 glEnableVertexAttribArray( 0 );
 glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(
 GLfloat ), ( GLvoid * )( 3 * sizeof( GLfloat ) ) );
 glEnableVertexAttribArray( 1 );
 glVertexAttribPointer( 2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(
 GLfloat ), ( GLvoid * )( 6 * sizeof( GLfloat ) ) );
 glEnableVertexAttribArray( 2 );
  1. 现在,让我们看看光照数组。在位置属性中,将 6 改为 8,原因与之前代码中提到的类似。

  2. 你不必在我们进行投影之前做这件事,但我们会的。我们只是在定义投影矩阵之前加载纹理。我们还将创建漫反射和镜面反射图,因为你只加载了两个不同的纹理。我们之前已经讨论过这个问题。所以,我们将添加 GLuintdiffuseMapspecularMap。然后我们添加 glGenTextures();。我们将传递参数 size1,对于 pointer,我们将添加 &diffuseMap 并复制粘贴此代码以节省时间。我们将对复制的代码进行以下更改:

// Load textures
 GLuint diffuseMap, specularMap;
 glGenTextures( 1, &diffuseMap );
 glGenTextures( 1, &specularMap );
  1. 现在,我们需要创建一个用于纹理宽度和高度的整型变量。

  2. 然后我们将添加 unsigned char *image。这本质上将是我们的图像数据,因为如果你尝试在某种文本编辑器中打开图像,你只会得到一串字符。这正是它将要存储的内容:

int textureWidth, textureHeight;
unsigned char *image;
  1. 因此,现在我们将添加漫反射图。我们首先添加 image = SOIL_LOAD_IMAGE();。对于这个,首先我们需要指定图像的文件路径,它是 res/images/container2.png。对于宽度和高度参数,我们只需指定我们之前创建的 &textureWidth&textureHeight,因为这是以引用的形式传递的,它实际上会修改这里的原始变量。对于通道,输入 0。对于 force_channels,只需输入 SOIL_LOAD_RGB

  2. 在下一行,我们需要添加 glBindTexture();。我们将传递的目标参数是 GL_TEXTURE_2D,对于纹理,我们只需指定 diffuseMap,因为我们目前正在使用它。

  3. 现在,在下一行我们需要添加 glTexImage2D();。我们将传递的目标参数是 GL_TEXTURE_2D。对于级别,输入 0。对于内部格式,这是 Gl_RGB,因为它没有 alpha 通道。对于宽度,你只需输入 textureWidth 然后输入 textureHeight。对于边框,输入 0。对于格式,输入 GL_RGB。对于类型,我们将输入 GL_UNSIGNED_BYTE。对于 pixels,只需指定图像数据,即 image

  4. 接下来,我们只需生成米普图,所以添加 glGenerateMipmap();,并且我们将传递 GL_TEXTURE_2D

  5. 然后添加 SOIL_free_image_data()。在这里,我们将指定我们想要释放的图像。

  6. 接下来,我们只需指定纹理参数,包括包装和过滤。因此,我们将添加 glTextParameteri();。为此,我们将传递目标参数为 GL_TEXTURE_2D。对于 name,我们现在修改的是包装,所以传递 GL_TEXTURE_WRAP_S a 然后传递 GL_REPEAT

  7. 让我们复制这段代码并将其粘贴在下面。查看以下代码以了解我们需要进行的修改:

 // Diffuse map
 image = SOIL_load_image( "res/images/container2.png", &textureWidth, &textureHeight, 0, SOIL_LOAD_RGB );
 glBindTexture( GL_TEXTURE_2D, diffuseMap );
 glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, textureWidth, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image );
 glGenerateMipmap( GL_TEXTURE_2D );
 SOIL_free_image_data( image );
 glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );
 glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT );
 glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR );
 glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST_MIPMAP_NEAREST );
  1. 现在我们已经完成了漫反射图的定义。接下来我们要做的是实际上复制所有这些代码用于镜面图,因为它会容易得多,而且其中很多都将保持不变。看看以下突出显示的术语,以了解变化:
// Specular map
 image = SOIL_load_image( "res/images/container2_specular.png", &textureWidth, &textureHeight, 0, SOIL_LOAD_RGB );
 glBindTexture( GL_TEXTURE_2D, specularMap );
 glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, textureWidth, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image );
 glGenerateMipmap( GL_TEXTURE_2D );
 SOIL_free_image_data( image );
 glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );
 glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT );
 glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR );
 glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST_MIPMAP_NEAREST );
 glBindTexture( GL_TEXTURE_2D, 0 ); 

在前面的代码中,我们在最后通过定义glBindTexture( GL_TEXTURE_2D, 0 );来解绑纹理。

  1. 现在,我们只需要设置光照着色器的纹理单元。所以,我们将添加lightingShader.Use();并在下一行添加glUniform1i,我们将指定glGetUniformLocation lightingShader.Program。我们只需要传递material.diffuseo。这些都是我们在着色器中做的,所以如果你只是需要快速提醒,可以再次查看。我们现在一切都准备好了:
// Set texture units
 lightingShader.Use( );
 glUniform1i( glGetUniformLocation( lightingShader.Program, "material.diffuse" ), 0 );
 glUniform1i( glGetUniformLocation( lightingShader.Program, "material.specular" ), 1 );

修改 while 循环

我们实际上可以在 while 循环中开始编写代码:

  1. 我们将设置光源的属性。所以,我们要做的是删除所有的lightColor代码,并添加glUniform3f();。我们将传递glGetUniformLocation(),并指定lightingShader.Program,我们只需要指定我们正在修改的第一个方面,即light. ambient,我们只是在这里放入一些硬编码的值:0.2f0.2f0.2f

  2. 让我们复制这个,这样我们就有了三个实例,并对它进行以下修改:

 // Set lights properties
 glUniform3f( glGetUniformLocation( lightingShader.Program, "light.ambient" ), 0.2f, 0.2f, 0.2f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "light.diffuse" ), 0.5f, 0.5f, 0.5f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "light.specular" ), 1.0f, 1.0f, 1.0f );
  1. 所以现在,让我们按照以下方式设置材质属性:
 // Set material properties
 glUniform1f( glGetUniformLocation( lightingShader.Program, "material.shininess"), 32.0f );
  1. 现在我们需要真正激活我们的纹理,并将它们绑定。所以,在glUniformMatrix4fv();下面,我们将添加以下代码:
// Bind diffuse map
    glActiveTexture( GL_TEXTURE0 );
    glBindTexture( GL_TEXTURE_2D, diffuseMap );
  1. 你可以复制并粘贴以下内容用于绑定镜面纹理图:
// Bind specular map
     glActiveTexture( GL_TEXTURE1 );
     glBindTexture( GL_TEXTURE_2D, specularMap ); 

现在我们已经准备好运行它了。你可能会在屏幕上观察到以下输出:

移动立方体;你会看到,当我们移动时,光照以不同的方式影响形状,因为我们正面看它时,几乎没有光泽。在右上角有一点;那就是我们移动的时候。它以逼真的方式影响我们的物体。当你移动光源时,你可以看到,只有金属部分在发光:

显然,这取决于你从哪个角度观察这个物体,因为在现实生活中就是这样。所以,这就是灯光图的全部内容。

摘要

在本章中,我们学习了如何给我们的物体着色,并在游戏世界中创建一个光源,比如灯。然后我们研究了光线对材料的影响。我们还了解了不同的光照技术:环境光、漫反射、镜面光照。我们探讨了各种材料,并观察了光线对材料的影响。我们通过学习本章中的光照图来结束。

在下一章中,我们将讨论不同光源的种类,例如方向光、点光源和聚光灯,以及如何在我们的游戏世界中将这些光源结合起来。

第五章:光源类型和灯光的组合

在本章中,我们将讨论各种光照效果类型,例如漫反射光、环境光和镜面反射光。您还将探索不同类型的光源,例如方向性光源、点光源和聚光灯。我们还将讨论如何将这不同类型的光源结合到您的游戏世界中。

本章我们将涵盖以下主题:

  • 实现不同类型的光源,例如方向性光源、点光源和聚光灯

  • 理解不同类型的光照效果,例如漫反射光、环境光和镜面反射光

  • 如何在您的游戏世界中组合不同类型的光照效果和光源

让我们开始吧。

您可以在 GitHub 上的Chapter05文件夹中找到本章的所有代码文件。GitHub 链接可以在书的序言中找到。

方向性光源

在本节中,我们将讨论方向性光源。我们现在已经相当深入地了解了 OpenGL 中可以使用的不同光照机制。我们探讨了光照贴图,以便能够照亮物体,并根据特定物体或物体特定部分的材料类型以不同的方式影响物体。

方向性光源

我们已经探讨了其他基本材料和基本光照,但你在游戏中可以使用几种主要的光源类型,例如方向性光源、点光源和聚光灯。我们将在后面的章节中介绍点光源和聚光灯;但方向性光源通常是 3D 图形中最基本的光源类型:

图片

因此,正如您在前面的图中可以看到的,有一些箭头从一个光源发出。方向性光源没有原点,或者更准确地说,没有位置,因为光源无限远。

例如,假设您有五个立方体。无论它们的材质如何,我们假设它们都是相同的,并且以相同的方式旋转,但位置各不相同。所以,让我们假设它们彼此之间相距 100 英里,并且朝任何方向。实际上,光和方向性光将以相同的方式影响每个单独的立方体对象,因为方向性光源没有起始位置。您无法接近光源;您可能会认为如果您朝光源的方向移动,您会接近光源。技术上,您可以说这是真的。但如果它没有原始位置和原始位置,并且无限远,它仍然会无限远。所以这就是方向性光源。

方向光在场景中类似于你拥有的普通光,然后你使用聚光灯和点光源来增强你的场景,创建更具体的东西。所以,让我们考虑这个例子。如果你有一个设定在平坦地面或某个岛屿上的游戏,那么方向光可以是太阳。

如果你不是真正垂直向上,你没有进入太空,你不能真正地靠近太阳;那么你可以将其视为方向光,在许多游戏中,这通常被认为是方向光。

再次,这取决于你玩的游戏类型。如果你玩的游戏可以进入太空并到达星星或太阳,那么这根本就不是方向光;那将是另一种光。但不同类型的光将在不同的部分中介绍。所以,让我们通过使用shaders/lighting.frag文件来实现这一点,如下面的代码所示:

struct Light
{
 //vec3 position;
 vec3 direction;

 vec3 ambient;
 vec3 diffuse;
 vec3 specular;
};

我们所做的是注释掉原始位置vec3 position,而不是添加一个方向,即vec3 direction

在漫反射光照中,我们需要稍作修改:

 // Diffuse
 vec3 norm = normalize(Normal);
 // vec3 lightDir = normalize(light.position - FragPos);
 vec3 lightDir = normalize(-light.direction);
 float diff = max(dot(norm, lightDir), 0.0);
 vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));

所以,让我们只注释掉lightDir,因为尽管我们还将有一个光方向,但我们想保留这段代码以备将来使用。所以,添加一个新的代码行,lightDir = normalize -light.direction

所以,这就是我们在这里需要做的全部,因为我们不是在计算位置(light.position)和实际片段着色器位置(FragPos)之间的差异。我们不需要这样做,因为我们只关心光的方向。

修改主代码以整合我们的世界中的方向光

现在打开main.cpp文件,该文件用于实际的光照和着色器。注释掉我们的着色器程序,因为我们在这个部分实际上不会使用灯着色器,仅仅是因为我们不希望有任何具有原点位置的光源:

// Build and compile our shader program
 Shader lightingShader( "res/shaders/lighting.vs",  
 "res/shaders/lighting.frag" );
 //Shader lampShader( "res/shaders/lamp.vs", "res/shaders/lamp.frag" );

在这里,我们将使用不同立方体位置的一个数组。

// Positions all containers
 glm::vec3 cubePositions[] = {
 glm::vec3( 0.0f, 0.0f, 0.0f),
 glm::vec3( 2.0f, 5.0f, -15.0f),
 glm::vec3( -1.5f, -2.2f, -2.5f),
 glm::vec3( -3.8f, -2.0f, -12.3f),
 glm::vec3( 2.4f, -0.4f, -3.5f),
 glm::vec3( -1.7f, 3.0f, -7.5f),
 glm::vec3( 1.3f, -2.0f, -2.5f),
 glm::vec3( 1.5f, 2.0f, -2.5f),
 glm::vec3( 1.5f, 0.2f, -1.5f),
 glm::vec3( -1.3f, 1.0f, -1.5f)
 };

我们将生成几个立方体,非常类似于我们之前所做的那样,你可以看到不同立方体之间的差异。我们不再需要光照顶点数组对象,因为我们不做灯着色器,所以让我们只注释掉它:

/*
 // Then, we set the light's VAO (VBO stays the same. After all, the vertices are the same for the light object (also a 3D cube))
 GLuint lightVAO;
 glGenVertexArrays(1, &lightVAO);
 glBindVertexArray(lightVAO);
 // We only need to bind to the VBO (to link it with glVertexAttribPointer), no need to fill it; the VBO's data already contains all we need.
 glBindBuffer(GL_ARRAY_BUFFER, VBO);
 // Set the vertex attributes (only position data for the lamp))
 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0); // Note that we skip over the other data in our buffer object (we don't need the normals/textures, only positions).
 glEnableVertexAttribArray(0);
 glBindVertexArray(0);
 */

到目前为止,一切看起来都很好,但我们需要在while循环中进行一些更改,到我们使用光照着色器的部分。所以,添加GLint lightDirLoc = glGetUniformLocationglUniform3f

// Use cooresponding shader when setting uniforms/drawing objects
 lightingShader.Use( );
 //GLint lightPosLoc = glGetUniformLocation(lightingShader.Program, "light.position");
 GLint lightDirLoc = glGetUniformLocation( lightingShader.Program, "light.direction" );
 GLint viewPosLoc = glGetUniformLocation( lightingShader.Program, "viewPos" );
//glUniform3f(lightPosLoc, lightPos.x, lightPos.y, lightPos.z);
glUniform3f( lightDirLoc, -0.2f, -1.0f, -0.3f );
glUniform3f( viewPosLoc, camera.GetPosition( ).x    , camera.GetPosition( ).y, camera.GetPosition( ).z );

视图位置位置,也就是摄像机的位置,是好的。环境光和漫反射不需要更改。如果你想修改它们,也可以,但在这个特定的章节中不是必需的。

现在,创建一个如下所示的循环:

 // Draw 10 containers with the same VAO and VBO information; 
// only their world space coordinates differ
 glm::mat4 model;
 glBindVertexArray( boxVAO );
 for ( GLuint i = 0; i < 10; i++)
 {
 model = glm::mat4( );
 model = glm::translate( model, cubePositions[i] );
 GLfloat angle = 20.0f * i;
 model = glm::rotate( model, angle, glm::vec3( 1.0f, 0.3f, 0.5f ) );
 glUniformMatrix4fv( modelLoc, 1, GL_FALSE, glm::value_ptr( model ) );

 glDrawArrays( GL_TRIANGLES, 0, 36 );
 }
 glBindVertexArray( 0 );

在这里,我们添加glm::mat4作为一个 4x4 矩阵。我们将称之为模型。然后我们添加glBindVertexArray,它将获取盒子顶点数组对象。有时我们只想有一个立方体;有时我们想有多个,所以我们将使用cubePositions,它将迭代器作为索引。现在我们将添加GLfloat angle = 20.0f * i; model = glm::rotate,对于旋转,它将再次使用模型。对于角度,我们只是放置角度。之后,我们将添加一个向量,glm::vec3,对于这个,我们只是放置1.0f0.3f0.5f。我们只是将 4x4 矩阵统一化。使用glUniformMatrix4fv,因为它有四个浮点值和modelLoc,这是我们之前创建的,它将取值为1。然后添加GL_FALSEglm::value_ptr (model)glDrawArrays将取GL_TRIANGLES,起始索引036个不同的顶点。所以,如果我们在这里放置一个分号,我们只需要将顶点数组绑定到0,所以我们只是取消绑定它。

现在我们将运行这个家伙,结果却是构建失败。这是因为我们注释掉了光顶点数组对象,因此我们也就没有必要使用glDeleteVertexArrays( 1, &lightVAO )了。所以,现在再次运行它,你将得到构建成功,并看到以下输出:

图片

在这里,我们有我们的不同对象。所有这些对象都受到光照的影响方式相同。显然,角度不同,这就是光照影响它的强度不同的原因。但在它们的位置方面,这并不重要。因为光照来自一个方向,物体受到的影响完全相同。它们既不暗也不亮。镜面光照将以相同的方式影响它们,并且是角度使得光照影响它不同。

到目前为止,我们已经学习了现代 OpenGL 中的方向光。在下一节中,我们将介绍一种更高级的光照形式,我们将使用光照创建一些非常酷的东西。当你看到我们的所有游戏,尤其是那些在图形保真度方面看起来非常棒的游戏时,你会发现,当你开始深入到这些游戏的底层并开始进行图形编程时,你会看到很多都是由于光照造成的。

光照影响事物的方式可能具有最大的影响,因为通过使用正常光照,你可以使一个物体看起来有深度,即使它只是一个平面纹理,这相当激进。

点光源

在本章中,我们将讨论点光源。到目前为止,我们已经对光照系统进行了深入的探讨。一种技术是方向性光源,本质上是指向某个方向的光源,因此得名方向性光源;但它们没有原始位置,也就是说,它们在无限远处。所以,比如说,我们有两个完全相同的物体,以完全相同的方式旋转,并且没有其他光照影响这些物体;无论它们彼此距离多远,它们都不会受到方向性光源的不同影响。

点光源的概念

点光源是一种具有实际起源的光源,它向每个方向发出光:

你几乎可以将一个点光源想象成我们生活中的太阳或星星。你可以争论说,从技术上讲,太阳从不同的侧面发出不同数量的光,但为了争论,我们可以这样说,它从其起源向所有方向发出相同强度、相同类型的光。在空间游戏中,像太阳、星星或其他类似物体一样有一个点光源是非常常见的。

首先,只需打开你的光照片段着色器,即../shaders/lighting.frag,在这个文件中,我们需要修改一些东西:

struct Light
{
 //vec3 direction;
 vec3 position;

 vec3 ambient;
 vec3 diffuse;
 vec3 specular;

 float constant;
 float linear;
 float quadratic;
};

在前面的代码片段中,我们将注释掉direction向量,因为我们不再需要方向,因为光源有一个位置,它只是向每个方向发出光。所以,ambientdiffusespecular将不需要更改。现在,我们将添加一个常数、一个线性和一个二次浮点数。这就是灯光结构体的全部内容。

扩散部分

现在,让我们检查一下扩散部分:

// Diffuse
 vec3 norm = normalize(Normal);
 vec3 lightDir = normalize(light.position - FragPos);
 //vec3 lightDir = normalize(-light.direction); 
 float diff = max(dot(norm, lightDir), 0.0);
 vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, 
 TexCoords));

我们仍然将规范化我们的Normal,但我们想要光位置和片段位置之间的差异。所以我们将注释掉光方向,vec3 lightDir = normalize(-light.direction),我们将取消注释light.position - FragPos

差异,max (dot( norm, lightDir), 将保持不变。在diffuse方面,我们仍在使用light.diffuse,将其乘以float diffuse变量,diff,然后是vec3

镜面反射部分

让我们现在看看镜面反射部分:

// Specular
 vec3 viewDir = normalize(viewPos - FragPos);
 vec3 reflectDir = reflect(-lightDir, norm);
 float spec = pow(max(dot(viewDir, reflectDir), 0.0),   
 material.shininess);
 vec3 specular = light.specular * spec * 
 vec3(texture(material.specular, TexCoords)); 

在这里,视方向和反射方向不会改变。我们需要添加的是称为衰减的东西,类似于距离和光照。

衰减部分

衰减实际上是一种断开连接。衰减光照是随着你远离物体而光照的减少。看看这张图片:

我们有四个不同的光源,ABCD。我们将假设它们都是相同的。A对地面的影响比D更大,因为D更远。所以,基本上衰减就是衰减。

因此,让我们来实现它:

// Attenuation
 float distance = length(light.position - FragPos);
 float attenuation = 1.0f / (light.constant + light.linear * distance
 + light.quadratic * (distance * distance)); 

在前面的代码片段中,如果光源距离更远但角度相同,它仍然会以相同的方式影响我们的对象,而我们不希望这样。因此,我们将添加float distance = length(light.position - FragPos),然后添加float attenuation = 1.0f / (light.constant + light.linear * distance + light.quadratic * (distance * distance))。所以,这就是我们计算距离和衰减的方法。

如果你想了解更多关于它是如何工作的信息,请随意查看以下链接:

这些是很好的资源,所以请随意查看它们。

现在我们将添加环境光、漫反射和镜面光的衰减:

 // Attenuation
 float distance = length(light.position - FragPos);
 float attenuation = 1.0f / (light.constant + light.linear * 
 distance + light.quadratic * (distance * distance));

 ambient *= attenuation;
 diffuse *= attenuation;
 specular *= attenuation;

我们需要这些,因为所有三个都需要考虑衰减,因为它们需要考虑距离。

main.cpp 中的更改时间

打开main.cpp文件。我们需要对其进行一些修改。首先,我们需要实际取消注释lampShader

// Build and compile our shader program
 Shader lightingShader( "res/shaders/lighting.vs", 
 "res/shaders/lighting.frag" );
 Shader lampShader( "res/shaders/lamp.vs", "res/shaders/lamp.frag" );

这是因为我们再次使用灯,因为我们正在处理点光源。

下一个我们需要改变的是光顶点数组对象,因为我们现在正在使用灯着色器。所以,取消注释以下代码块:

 GLuint VBO, boxVAO;
 glGenVertexArrays( 1, &boxVAO );
 glGenBuffers( 1, &VBO );

 glBindBuffer( GL_ARRAY_BUFFER, VBO );
 glBufferData( GL_ARRAY_BUFFER, sizeof(vertices), 
 vertices, GL_STATIC_DRAW );

 glBindVertexArray( boxVAO );
 glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 8 * 
 sizeof( GLfloat ), ( GLvoid * )0 );
 glEnableVertexAttribArray(0);
 glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, 8 * 
 sizeof( GLfloat ), ( GLvoid * )( 3 * sizeof( GLfloat ) ) );
 glEnableVertexAttribArray( 1 );
 glVertexAttribPointer( 2, 2, GL_FLOAT, GL_FALSE, 8 * 
 sizeof( GLfloat ), ( GLvoid * )( 6 * sizeof( GLfloat ) ) );
 glEnableVertexAttribArray( 2 );
 glBindVertexArray( 0 );

现在我们需要一个发射贴图,所以我们将添加emissionMap

 // Load textures
 GLuint diffuseMap, specularMap, emissionMap;
 glGenTextures( 1, &diffuseMap );
 glGenTextures( 1, &specularMap );
 glGenTextures( 1, &emissionMap );

现在我们需要做的唯一改变实际上是在我们的while循环中:

 lightingShader.Use( );

 GLint lightPosLoc = glGetUniformLocation( lightingShader.Program, 
 "light.position" );
 //GLint lightDirLoc = glGetUniformLocation( lightingShader.Program,    
 //"light.direction" );
 GLint viewPosLoc = glGetUniformLocation( lightingShader.Program, 
 "viewPos" );
 glUniform3f( lightPosLoc, lightPos.x, lightPos.y, lightPos.z );
 //glUniform3f( lightPosLoc, -02.f, 1.0f, -0.3f );
 glUniform3f( viewPosLoc, camera.GetPosition( ).x, camera.GetPosition( ).y, camera.GetPosition( ).z );
 and comment out the directions, GLint lightDirLoc = glGetUniformLocation( lightingShader.Program, "light.direction" );, because again, the pointer light emits light in every direction. Also, don't forget to comment out glUniform3f( lightPosLoc, -02.f, 1.0f, -0.3f ); and remove the comment from glUniform3f( lightPosLoc, lightPos.x, lightPos.y, lightPos.z );

现在,我们还需要添加常量、线性项和二次项,这些都是浮点值:

// Set lights properties
 glUniform3f( glGetUniformLocation( lightingShader.Program, "light.ambient" ), 0.2f, 0.2f, 0.2f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "light.diffuse" ), 0.5f, 0.5f, 0.5f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "light.specular" ), 1.0f, 1.0f, 1.0f );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "light.constant" ), 1.0f );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "light.linear" ), 0.09 );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "light.quadratic" ), 0.032 );

现在,为了绑定我们的纹理,我们需要取消注释lampShader.Use( )块。同时,删除光顶点数组对象,即取消注释glDeleteVertexArrays( 1, &lightVAO );行:

 glDeleteVertexArrays( 1, &boxVAO );
 glDeleteVertexArrays( 1, &lightVAO );
 glDeleteBuffers( 1, &VBO );

现在,我们准备运行我们的应用程序,我们得到以下屏幕:

如果检查输出,我们得到了原始的光源,并且距离较远的对象稍微暗一些,正如它们应该的那样。

但让我们在main.cpp中再做一个更改;我们实际上可以通过在while循环中取消注释以下行来移动我们的光:

  • lightPos.x -=0.005f;

  • lightPos.z -=0.005f;

现在,如果你重新运行应用程序,正如你在以下屏幕截图中所见,衰减已经考虑在内。你可以看到这个对象现在稍微亮一些,你实际上会开始看到这个特定的对象变亮:

现在,再次在while循环中取消注释以下行:

  • lightPos.x -=0.005f;

  • lightPos.z -=0.005f;

而我们将尝试在相机部分改变光的位置:

// Camera
Camera camera( glm::vec3( 0.0f, 0.0f, 3.0f ) );
GLfloat lastX = WIDTH / 2.0;
GLfloat lastY = HEIGHT / 2.0;
bool keys[1024];
bool firstMouse = true;

// Light attributes
glm::vec3 lightPos( 1.2f, 1.0f, -2.0f );

所以,正如你可以在前面的代码中看到的那样,我们在光属性部分将2.0f替换为-2.0f。现在,如果你运行你的应用程序,你可以看到光正在向每个方向发射。较近的对象比远处的对象更亮:

所以,这就是点光源的全部内容。

聚光灯

让我们讨论并看看我们如何将聚光灯添加到我们的游戏中。我们已经看过方向光,我们也看过点光源。方向光有一个方向,但没有原始位置,所以它是无限远的。点光源有一个位置,但它向每个方向发光,而聚光灯有一个位置和方向。

看看以下聚光灯的图示:

所以,光源的位置很高,你还可以看到有一个光的方向。它本质上创建了一个圆锥形的效果,就像灯或火炬一样。聚光灯在舞台上使用。但在你的世界中,聚光灯在游戏的多个场景中使用。

所以,无需多言,让我们开始编写我们的聚光灯代码。

修改着色器文件

按照以下步骤进行:

  1. 更新着色器文件实际上非常非常简单。我们只需要实际修改着色器文件中的光照片段着色器;其他所有内容看起来都很好。在lighting.frag中,我们可以保持Material结构体不变,因为它有diffusespecularshininess,这正是我们需要的聚光灯特性。

  2. 但是,与光结构体一起,我们需要方向,因为聚光灯有一个原始位置和它照射的方向。所以,我们将取消注释vec3 direction。我们还需要几个浮点变量。第一个是float cutOff。下一个是float outerCutOff。看看以下代码:

#version 330 core
struct Material
{
     sampler2D diffuse;
     sampler2D specular;
     float shininess;
};
struct Light
{
     vec3 position;
     vec3 direction;
     float cutOff;
 float outerCutOff;

     float constant;
     float linear;
     float quadratic;

     vec3 ambient;
     vec3 diffuse;
     vec3 specular;
};
  1. void main开始之前的其余术语保持不变。

  2. void main中,环境计算、漫反射计算和镜面反射计算都没有变化。甚至衰减计算也不会变化。我们实际上需要做的只是为聚光灯添加一个额外的部分,这将计算柔和的边缘。

  3. 所以,对于柔和边缘的计算,我们将添加float theta = dot();,并将lightDir传递给它。这需要被归一化,所以我们传递normalize()。然后最后,在这里你需要指定-light.direction。你需要添加一个负的光值,因为你是从摄像机的角度而不是从用户的角度来做的。这就是为什么位置被取反。在下一行,我们需要计算cutOffouterCutOff之间的差异,所以我们将添加以下内容:

float epsilon = (light.cutOff - light.outerCutOff);
  1. 然后添加float intensity = clamp();。在这里,我们将传递theta - light.outerCutOff。我们想将这个计算除以epsilon,然后只放两个值,0.01.0
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
  1. 最后,我们只需要将强度添加到diffusespecular中:
diffuse *= intensity;
specular *= intensity;

所以,我们现在实际上已经完成了着色器的更新。

对 Camera.h 进行微小修改

在本节中,我们需要 GetFront,这是主代码中的一个私有变量。我们将在 Camera.h 文件中对它进行一些小的修改。所以,在 glm::vec3 GetPosition() 方法下面,我们将添加一个简单的方法,如下所示:

glm::vec3 GetFront()
{
    return this -> front;
}

修改主代码

现在,如果我们转到 main.cpp,我们将进行以下修改:

  1. 我们不需要 lampShader,因为我们将通过聚光灯来完成,所以我们将注释掉代码。我们不需要任何类型的灯片着色器或其他类似的东西,因为我们在这个部分要做的是将聚光灯附着在本质上相当于相机的位置。你几乎可以把它想象成那些顶部带有灯的头盔,人们用来攀岩和类似的活动。我们只是简单地模拟这个效果,因为方向光和点光源很棒,如果你让它们保持静态,它们很容易被看到和理解正在发生的事情。有了聚光灯,如果你能移动它,那真的很有帮助,而移动它的最好方法就是使用相机。

  2. 我们将注释掉光顶点数组对象,因为我们不再需要它了。

  3. 在注释掉这些代码之后,我们可以直接进入循环,并需要在这里做一些修改。当我们到达 lightingShader.Use 时,我们需要更改那里的某些代码。我们将添加 GLint lightSpotDirLocation = glGetUniformLocation();。在这里,我们需要传递 lightingShader.Program,另一个需要指定的参数是 "light.direction"。我们接下来要做的是复制前面的代码,并对其进行以下修改:

 lightingShader.Use();
 GLint lightPosLoc = glGetUniformLocation( lightingShader.Program, "light.position" );
 GLint lightSpotdirLoc = glGetUniformLocation( lightingShader.Program, "light.direction" );
 GLint lightSpotCutOffLoc = glGetUniformLocation( lightingShader.Program, "light.cutOff" );
 GLint lightSpotOuterCutOffLoc = glGetUniformLocation( lightingShader.Program, "light.outerCutOff" );
 GLint viewPosLoc = glGetUniformLocation( lightingShader.Program, "viewPos" );
 glUniform3f( lightPosLoc, camera.GetPosition( ).x, camera.GetPosition( ).y, camera.GetPosition( ).z);
 glUniform3f( lightSpotdirLoc, camera.GetFront( ).x, camera.GetFront( ).y, camera.GetFront( ).z);
 glUniform1f( lightSpotCutOffLoc, glm::cos( glm::radians( 12.5f ) ) );
 glUniform1f( lightSpotOuterCutOffLoc, glm::cos( glm::radians( 17.5f ) ) );
 glUniform3f( viewPosLoc, camera.GetPosition( ).x, camera.GetPosition( ).y, camera.GetPosition( ).z);
  1. 我们将修改 lightingShader 代码中剩余的术语,其中我们设置光属性如下:
glUniform3f( glGetUniformLocation( lightingShader.Program, "light.ambient" ),   0.1f, 0.1f, 0.1f );
glUniform3f( glGetUniformLocation( lightingShader.Program, "light.diffuse" ), 0.8f, 0.8f, 0.8f );

  1. 在这里,我们只改变一件事。我们将注释掉整个 lampShader 代码。因为我们已经注释掉了声明和初始化,所以在这里我们也需要这样做。

  2. 我们还需要注释掉 glDeleteVertexArrays();

我们现在可以运行这个程序了。你会在屏幕上看到类似的输出。所以,正如你所见,我们有一些光线。聚光灯附着在我们身上。

因此,如果我们向前移动,正如你所见,我们得到了一种聚光灯效果,而且越靠近它,效果越明显,如下面的截图所示:

否则,当我们远离物体时,它非常宽,如下面的截图所示:

因此,它们较小。当我们四处张望时,我们得到了一种聚光灯效果。正如你所见,它对远离物体的物体产生的影响略有不同,我们有一个非常酷的聚光灯,如下面的截图所示:

让我们尝试在角落处做一下,如下所示:

我们有一个非常酷的附着在我们头上的聚光灯。你可以继续创建某种建筑游戏或某种矿工游戏,其中你头上有一个光源。那会非常酷。

结合光线

在本节中,我们将探讨如何结合我们的光源。到目前为止,在前面几节中,我们已经介绍了方向光、点光源和聚光灯。以下是对它们的简要概述:

  • 方向光: 方向光是一种具有特定方向的光。它以特定方向发光,但没有位置,没有位置。它只是无限地远离一切。

  • 点光源: 点光源有一个位置,但它向每个方向发光。根据你所做的工作和你的游戏类型,你可能会将太阳或星星作为方向光,但如果你能进入太空,绕着你的星星或靠近它,那么你可能想要一个点光源。

  • 聚光灯: 聚光灯本质上就像一盏灯。它在初始位置投射光线,然后在方向上也是如此。所以它就像是前两种光类型的组合。

在前面的章节中,我们看了所有这些,但此刻,在这些章节中,我们只是逐个介绍它们。我们要么注释掉代码,要么修改代码,只是为了展示一个真实的情况。但在实际的游戏场景或自由应用程序场景中,你将想要多个光源。你将想要多个方向光、点光源和聚光灯的实例。你可能还想要尝试创建你自己的非常酷的效果。在本节中,我们将结合我们三种类型的光投射器。

如往常一样,我们将从更新着色器文件开始。

准备着色器文件

看一下下面提到的步骤:

  1. 我们需要做的第一件事是进入片段着色器中的照明部分,lighting.frag。这是我们实际上需要修改的唯一着色器,所以我们不会触摸lighting.vs。查看以下步骤以了解我们需要对片段着色器做出的更改:

  2. 因此,首先,我们将添加#define,这将是NUMBER_OF_POINT_LIGHTS。对于我们的项目,我们将添加值为 4,因为我们将有四个点光源。

  3. 接下来,我们需要为三种不同类型的光源创建一个数据类型:方向光、点光源和聚光灯。为此,我们将实际上复制代码中已有的结构体。我们将适当地重命名每一个:DirLight用于方向光,PointLight用于点光源,SpotLight用于聚光灯。我们不需要结构体中所有的向量和浮点数。查看以下代码以了解我们需要在定义的新结构体中做出的所有更改:

version 330 core
#define NUMBER_OF_POINT_LIGHTS 4
struct Material
{
     sampler2D diffuse;
     sampler2D specular;
     float shininess;
};
  1. 方向光,如您可能记得的,没有位置,它有一个方向。它有环境光、漫反射和镜面反射,但没有任何浮点变量:常数、线性二次。所以我们将只移除它们:
struct DirLight
{
     vec3 direction;

     vec3 ambient;
     vec3 diffuse;
     vec3 specular;
};
  1. 对于点光源,请记住它没有方向;它有一个位置,因为它只是向所有方向发射光。我们可以去掉cutOffouterCutOff,但我们需要其他所有东西:
struct PointLight
{
     vec3 position;

     float constant;
     float linear;
     float quadratic;

     vec3 ambient;
     vec3 diffuse;
     vec3 specular;
};
  1. 对于聚光灯,这里不会有任何变化,因为这个结构是在上一节中创建的,该节涵盖了聚光灯:
struct SpotLight
{
     vec3 position;
     vec3 direction;
     float cutOff;
     float outerCutOff;

     float constant;
     float linear;
     float quadratic;

     vec3 ambient;
     vec3 diffuse;
     vec3 specular;
};
  1. 随着我们向下移动,uniform Light light将略有变化,因为我们有三个不同的光源。所以我们将对其进行以下更改:
uniform DirLight dirLight;
uniform PointLight pointLights[NUMBER_OF_POINT_LIGHTS];
uniform SpotLight spotLight;
uniform Material material;

在前面的代码中,如您可能记得的,我们创建了一个#define。因此,uniform PointLight将是一个灯光数组。尽管我们创建了多个聚光灯和一个方向光以及聚光灯,但您也可以创建多个方向光和多个聚光灯,例如台灯、棍子上点的灯等。您可能有一个非常强烈的光源,例如模拟太阳的方向光,然后您可能只有其他一些普通的小灯。

  1. 接下来我们需要做的是创建一些函数原型,因为到目前为止,我们一直在main函数中做所有的事情。到目前为止这还不错,但我们需要在做事的方式上增加一些灵活性。所以,我们将添加vec3 CalcDirLight();,这将接受一些参数,例如DirLight。然后我们将取一个vec3作为法线。我们之前已经解释了所有这些不同向量和属性的使用。再次强调,我们只是在结合过去几节中我们所做的工作。然后,我们将复制代码并对其做出以下突出显示的更改:
// Function prototypes
vec3 CalcDirLight( DirLight light, vec3 normal, vec3 viewDir );
vec3 CalcPointLight( PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir );
vec3 CalcSpotLight( SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir );

lighting.fragvoid main中修改

查看以下步骤以了解修改:

  1. lighting.fragvoid main中,我们将移除所有最初存在的代码并添加新的代码。我们将从添加vec3 normal开始。我们只是使用Normal进行标准化。

  2. 然后,我们将为viewDir创建一个vec3。这将标准化viewPosFragPos之间的差异。

  3. 现在我们需要进行方向光计算。为此,我们只需调用CalcDirLight方法。所以,我们不会在main中做很多代码。因为我们正在向其中添加更多内容,它正变得像另一个 C++项目,因为我们正在将其抽象成不同的方法,这样我们就可以在需要的时候重用这段代码。所以,我们将添加vec3 result并将CalcDirLight();的值赋给它,这将接受dirLight变量、我们刚刚计算出的normviewDir

  4. 接下来,我们只是将点光源循环遍历,然后考虑它们。所以,添加一个 for 循环并传递初始化参数,int i = 0; i < NUMBER_OF_POINT_LIGHTS; i++。在 for 循环中,我们将添加 result += CalcPointLight()。我们正在做的是将不同点光源的效果添加到我们的结果中,因为这种光照再次影响我们的特定对象,这就是它的做法。所以,我们将 pointLights[i]normFragPos 传递给 CalcPointLight()

  5. 我们现在将添加聚光灯的代码。所以我们将考虑聚光灯并添加 result += CalcSpotLight()。这简单地接受 spotLight 变量、normFragPosviewDir。然后我们将添加 color = vec4(result,1.0);。请查看以下代码以了解描述:

void main( )
{
 // Properties
 vec3 norm = normalize( Normal );
 vec3 viewDir = normalize( viewPos - FragPos );

 // Directional lighting
 vec3 result = CalcDirLight( dirLight, norm, viewDir );

 // Point lights
 for ( int i = 0; i < NUMBER_OF_POINT_LIGHTS; i++ )
 {
 result += CalcPointLight( pointLights[i], norm, FragPos, viewDir );
 }

 // Spot light
 result += CalcSpotLight( spotLight, norm, FragPos, viewDir );

 color = vec4( result, 1.0 );
}
  1. 这都是我们之前做过的事情,我们现在只是将其抽象出来。

  2. 现在我们来计算不同光源颜色的计算。所以在这里,我们将复制并粘贴 vec3 calc 代码到所有三个光源。现在我们来计算方向光。所以我们将向 vec3 CalcDirLight() 方法添加 vec3 lightDir,这将等于 normalize( -light.direction )

-light.direction 的原因是因为我们不是从我们的对象出发来做的,而是从光线出发。所以,不是对象看光线的方向,而是光线从对象出发。这就是为什么它被翻转的原因。

  1. 现在我们需要添加 float。这将是要素阴影。所以 diff = max(),并将 dot( normal, lightDir ), 0.0 传递给 max()

  2. 接下来,我们将计算镜面反射阴影。所以添加 vec3 reflectDir = reflect(),并将 -lightDirnormal 传递给 reflect()

  3. 然后我们将添加 float spec = pow(),并将 max() 传递给 max(),其中包含 dot( viewDir, reflectDir )0.0

  4. 最后,我们需要添加 material.shininess。现在我们需要组合结果。所以添加 vec3 ambient = light.ambient * vec3 (),并将 vec3() 传递给 texture( material.diffuse, TexCoords )。这将是要素纹理的漫反射和纹理坐标,对于漫反射和镜面反射光,它与环境光类似,所以请在以下代码中做出突出显示的更改。同时,请查看以下代码以了解前面的描述:

 vec3 CalcDirLight( DirLight light, vec3 normal, vec3 viewDir )
{
 vec3 lightDir = normalize( -light.direction );

 // Diffuse shading
 float diff = max( dot( normal, lightDir ), 0.0 );

 // Specular shading
 vec3 reflectDir = reflect( -lightDir, normal );
 float spec = pow( max( dot( viewDir, reflectDir ), 0.0 ), material.shininess );

 // Combine results
 vec3 ambient = light.ambient * vec3( texture( material.diffuse, TexCoords ) );
 vec3 diffuse = light.diffuse * diff * vec3( texture( material.diffuse, TexCoords ) );
 vec3 specular = light.specular * spec * vec3( texture( material.specular, TexCoords ) );

 return ( ambient + diffuse + specular );
}

在前面的代码中,我们只需要返回计算结果,即 ambient + diffuse + specular

  1. 现在我们需要计算点光源,所以我们将复制前面步骤中提到的代码行,并将其粘贴到 CalcPointLight() 方法中;我们将添加、更改和删除所需的内容。请看以下突出显示的代码以了解更改:
// Calculates the color when using a point light.
vec3 CalcPointLight( PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir )
{
 vec3 lightDir = normalize( light.position - fragPos );

 // Diffuse shading
 float diff = max( dot( normal, lightDir ), 0.0 );

 // Specular shading
 vec3 reflectDir = reflect( -lightDir, normal );
 float spec = pow( max( dot( viewDir, reflectDir ), 0.0 ), material.shininess );

 // Attenuation
 float distance = length( light.position - fragPos );
 float attenuation = 1.0f / ( light.constant + light.linear * distance + light.quadratic * ( distance * distance ) );

 // Combine results
 vec3 ambient = light.ambient * vec3( texture( material.diffuse, TexCoords ) );
 vec3 diffuse = light.diffuse * diff * vec3( texture( material.diffuse, TexCoords ) );
 vec3 specular = light.specular * spec * vec3( texture( material.specular, TexCoords ) );

 ambient *= attenuation;
 diffuse *= attenuation;
 specular *= attenuation;

 return ( ambient + diffuse + specular );
}

在前面的代码中,在定义了 specular 阴影之后,我们添加了衰减代码,因为我们需要考虑衰减。

  1. 现在,我们需要对spotlight ()方法进行计算。同样,我们只是复制并粘贴之前的代码,因为我们很可能会添加很多东西,但我们需要大部分。所以请看以下突出显示的代码:
// Calculates the color when using a spot light.
vec3 CalcSpotLight( SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir )
{
 vec3 lightDir = normalize( light.position - fragPos );

 // Diffuse shading
 float diff = max( dot( normal, lightDir ), 0.0 );

 // Specular shading
 vec3 reflectDir = reflect( -lightDir, normal );
 float spec = pow( max( dot( viewDir, reflectDir ), 0.0 ), material.shininess );

 // Attenuation
 float distance = length( light.position - fragPos );
 float attenuation = 1.0f / ( light.constant + light.linear * distance + light.quadratic * ( distance * distance ) );

 // Spotlight intensity
 float theta = dot( lightDir, normalize( -light.direction ) );
 float epsilon = light.cutOff - light.outerCutOff;
 float intensity = clamp( ( theta - light.outerCutOff ) / epsilon, 0.0, 1.0 );

 // Combine results
 vec3 ambient = light.ambient * vec3( texture( material.diffuse, TexCoords ) );
 vec3 diffuse = light.diffuse * diff * vec3( texture( material.diffuse, TexCoords ) );
 vec3 specular = light.specular * spec * vec3( texture( material.specular, TexCoords ) );

 ambient *= attenuation * intensity;
 diffuse *= attenuation * intensity;
 specular *= attenuation * intensity;

 return ( ambient + diffuse + specular );
}

在前面的代码中,在我们继续到ambientdiffusespecular向量之前,我们需要添加聚光灯强度计算的代码。仔细查看突出显示的代码。然后,最后,我们只需要考虑强度。所以我们在代码的最后几行中只是将衰减乘以强度。

我们现在已经更新了片段光照着色器。我们之前已经覆盖了所有这些内容。我们现在只是将它们全部组合在一起。现在,我们已经完成了。我相信这里肯定会有错误,因为这里有很多代码,所以当我们开始编译它时我们会处理这些错误。所以让我们保存它。

现在,我们将继续修改我们的主要代码。

主代码的更改

按照以下步骤在我们的代码中组合光源:

  1. main.cpp中,我们需要灯的着色器,所以我们将取消注释它。在cubePositions之后,因为我们现在有多个点光源位置,我们将添加glm::vec3,这将被称为pointLightPositions[]。我们将在其中添加glm::vec3();,为此,我们将传递0.7f0.2f2.0f。复制这一行代码,粘贴四次,并做以下修改:
  // Positions of the point lights
     glm::vec3 pointLightPositions[] = 
{
     glm::vec3( 0.7f, 0.2f, 2.0f ),
     glm::vec3( 2.3f, -3.3f, -4.0f ),
     glm::vec3( -4.0f, 2.0f, -12.0f ),
     glm::vec3( 0.0f, 0.0f, -3.0f )
 };
  1. 接下来,我们将取消注释灯光顶点数组对象,因为我们现在需要它来组合灯光。

  2. 我们需要做的重大更改现在都在while循环中。我们正在使用光照着色器,所以我们将对其进行以下更改:

// Use cooresponding shader when setting uniforms/drawing objects
 lightingShader.Use( );
 GLint viewPosLoc = glGetUniformLocation( lightingShader.Program, "viewPos" );
 glUniform3f( viewPosLoc, camera.GetPosition( ).x, camera.GetPosition( ).y, camera.GetPosition( ).z);
 // Set material properties
 glUniform1f( glGetUniformLocation( lightingShader.Program, "material.shininess" ), 32.0f );
  1. 我们正在设置方向光的制服:
 // Directional light
 glUniform3f( glGetUniformLocation( lightingShader.Program, "dirLight.direction" ), -0.2f, -1.0f, -0.3f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "dirLight.ambient" ), 0.05f, 0.05f, 0.05f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "dirLight.diffuse" ), 0.4f, 0.4f, 0.4f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "dirLight.specular" ), 0.5f, 0.5f, 0.5f );
  1. 然后我们将为点光源 1 设置制服:
// Point light 1
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[0].position" ), pointLightPositions[0].x, pointLightPositions[0].y, pointLightPositions[0].z );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[0].ambient" ), 0.05f, 0.05f, 0.05f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[0].diffuse" ), 0.8f, 0.8f, 0.8f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[0].specular" ), 1.0f, 1.0f, 1.0f );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "pointLights[0].constant" ), 1.0f );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "pointLights[0].linear" ), 0.09f );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "pointLights[0].quadratic" ), 0.032f );
  1. 同样,为点光源 2 设置制服:
// Point light 2
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[1].position" ), pointLightPositions[1].x, pointLightPositions[1].y, pointLightPositions[1].z );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[1].ambient" ), 0.05f, 0.05f, 0.05f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[1].diffuse" ), 0.8f, 0.8f, 0.8f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[1].specular" ), 1.0f, 1.0f, 1.0f );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "pointLights[1].constant" ), 1.0f );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "pointLights[1].linear" ), 0.09f );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "pointLights[1].quadratic" ), 0.032f );
  1. 这里是点光源 3 的制服定义:
 // Point light 3
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[2].position" ), pointLightPositions[2].x, pointLightPositions[2].y, pointLightPositions[2].z );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[2].ambient" ), 0.05f, 0.05f, 0.05f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[2].diffuse" ), 0.8f, 0.8f, 0.8f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[2].specular" ), 1.0f, 1.0f, 1.0f );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "pointLights[2].constant" ), 1.0f );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "pointLights[2].linear" ), 0.09f );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "pointLights[2].quadratic" ), 0.032f );
  1. 这里是点光源 4 的定义:
// Point light 4
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[3].position" ), pointLightPositions[3].x, pointLightPositions[3].y, pointLightPositions[3].z );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[3].ambient" ), 0.05f, 0.05f, 0.05f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[3].diffuse" ), 0.8f, 0.8f, 0.8f );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "pointLights[3].specular" ), 1.0f, 1.0f, 1.0f );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "pointLights[3].constant" ),  1.0f );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "pointLights[3].linear" ), 0.09f );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "pointLights[3].quadratic" ), 0.032f );

  1. 然后我们将按照以下方式定义聚光灯的制服:
// SpotLight
 glUniform3f( glGetUniformLocation( lightingShader.Program, "spotLight.position" ), camera.GetPosition( ).x, camera.GetPosition( ).y, camera.GetPosition( ).z );
 glUniform3f( glGetUniformLocation( lightingShader.Program, "spotLight.direction" ), camera.GetFront( ).x, camera.GetFront( ).y, camera.GetFront( ).z );

glUniform3f( glGetUniformLocation( lightingShader.Program, "spotLight.ambient" ), 0.0f, 0.0f, 0.0f );

 glUniform3f( glGetUniformLocation( lightingShader.Program, 
"spotLight.diffuse" ), 1.0f, 1.0f, 1.0f );

 glUniform3f( glGetUniformLocation( lightingShader.Program, 
"spotLight.specular" ), 1.0f, 1.0f, 1.0f );

 glUniform1f( glGetUniformLocation( lightingShader.Program, "spotLight.constant" ), 1.0f );

 glUniform1f( glGetUniformLocation( lightingShader.Program, "spotLight.linear" ), 0.09f );

glUniform1f( glGetUniformLocation( lightingShader.Program, "spotLight.quadratic" ), 0.032f );

glUniform1f( glGetUniformLocation( lightingShader.Program, "spotLight.cutOff" ), glm::cos( glm::radians( 12.5f ) ) );
 glUniform1f( glGetUniformLocation( lightingShader.Program, "spotLight.outerCutOff" ), glm::cos( glm::radians( 15.0f ) ) );

在前面的代码行中,我们为 5 或 6 种类型的灯光设置了所有制服。我们必须手动设置它们,并在数组中索引正确的PointLight结构来设置每个制服变量。这可以通过定义灯光类型为类并在其中设置它们的值,或者通过使用更高效的制服方法,即使用制服缓冲对象来使代码更友好。

  1. 我们仍然有我们不再需要的所有代码,所以,从我们完成 spotlight 相关内容的点开始,到我们开始定义视图矩阵的点,我们需要删除所有这些代码。这些都是当只有一种灯光类型时的残留代码,它被称为Light,所以我们将删除它。

  2. 我们需要将着色器中的注释重新添加回去,并且需要将删除灯光顶点数组对象的注释重新添加回去。

  3. 我们已经遍历了我们的盒子数组,创建了所有不同的盒子,并且我们有了我们的灯着色器。记住,我们有多个点光源,所以我们只需要为它创建一个循环。所以,在绑定顶点数组和解绑它之后,我们需要添加 glBindVertexArray( lightVAO );

  4. 然后我们将添加我们的 for 循环,并将循环的初始化参数作为 GLuint i = 0; i < 4; i++ 传递,并将 model = glm::mat4(); 添加到循环中。

  5. 然后,在另一行,我们将添加 model = glm::translate();,这只是为了将 model 平移。然后这个向量的值将是 pointLightPositions。然后传递迭代器 [i]

  6. 现在,我们只是将立方体稍微缩小一点,就像我们之前做的那样。所以我们将添加 model = glm::scale();,并将 modelglm::vec3( 0.2f ) 传递给它。

  7. 在另一行我们将添加 glUniformMatrix4fv(),因为它是一个 4x4 矩阵,我们需要传递 modelLoc, 1, GL_FALSE;glm::value_ptr();到这个,我们将传递模型。

  8. 然后我们将添加 glDrawArrays();,并传递 GL_TRIANGLES036。在这段循环在另一行完成后,我们需要取消绑定顶点数组,即 glBindVertexArray( 0 );。请查看以下代码以了解描述:

// We now draw as many light bulbs as we have point lights.
 glBindVertexArray( lightVAO );
 for ( GLuint i = 0; i < 4; i++ )
 {
 model = glm::mat4( );
 model = glm::translate( model, pointLightPositions[i] );
 model = glm::scale( model, glm::vec3( 0.2f ) ); // Make it a smaller cube
 glUniformMatrix4fv( modelLoc, 1, GL_FALSE, glm::value_ptr( model ) );
 glDrawArrays( GL_TRIANGLES, 0, 36 );
 }
 glBindVertexArray( 0 );

我们现在已经准备好运行代码了。保存更新后的代码并编译它。你将在屏幕上看到类似以下输出:

图片

我们有多个光源,其中四个是点光源,一个是普通的灯。我们建议你找出哪些是点光源,哪个是灯。正如你所见,我们有一个附加的聚光灯,还有一个普通的方向光。你可能认为很难分辨出哪种光是什么,在现实世界中通常也是如此。光会影响我们周围的一切,这就是它在游戏中的工作方式。现在,如果你试图远离物体,我们的聚光灯实际上已经不再影响立方体了,但它仍然在那里,如下面的截图所示:

图片

随着我们越来越接近它,它开始影响物体:

图片

看起来很酷,可以看到效果是如何与其他光源结合的。所以,这就是结合方向光、点光和聚光灯的方法。这一章有很多冗长的代码,但我们已经在前面的章节中做了很多。

摘要

在这一章中,我们讨论了不同类型的光源,如方向光、点光和聚光灯。然后我们学习了如何将这些光源和光照效果结合起来,以在我们的游戏世界中生成逼真的光照。

在下一章中,我们将讨论立方体贴图,并学习如何为我们游戏生成天空盒。

第六章:使用立方体贴图实现天空盒

在本章中,我们将使用立方体贴图创建一个天空盒。因此,让我们首先了解什么是立方体贴图。它是由多个纹理组合成单个纹理,形成一个立方体。它基本上是一系列六个单独的 2D 纹理,它们被映射到一个立方体上。它们通常会有某种图案,以这种方式从一侧流动到另一侧。天空盒本质上是一个立方体贴图,但非常大。玩家和游戏世界基本上位于这个大立方体内部。它包含整个场景的六个游戏环境图像;如果你作为一个玩家,身处天空盒中并试图环顾四周,你会感觉周围有一个高分辨率的宇宙。而且,如果你试图触摸立方体的边缘,你将无法做到,因为它离你无限远。在本章中,我们将学习如何使用立方体贴图实现天空盒,以在你的游戏中创建令人惊叹的世界。

我们将首先为我们的天空盒创建着色器。

你可以在 GitHub 的 Chapter06 文件夹中找到本章的所有代码文件。GitHub 链接可以在书的序言中找到。

为天空盒创建着色器

如往常一样,我们将从创建我们的着色器开始。我们将复制我们的着色器文件 core.vscore.frag,并将这些复制的文件命名为 skybox.vsskybox.frag。现在,我们将对这些着色器文件进行一些修改;看看以下步骤,以了解将要进行的更改:

  1. 我们将从修改我们的 skybox.vs 着色器开始。看看以下代码,并在你的着色器文件中实现以下修改:
#version 330 core 

layout (location = 0) in vec3 position; 

out vec3 TexCoords; 
uniform mat4 projection; 
uniform mat4 view; 
void main() 

{ 
    vec4 pos = projection * view * vec4(position, 1.0); 
    gl_Position = pos.xyww; 
    TexCoords = position; 
} 

修改完成后,保存文件。

  1. 接下来,我们将转到 Skybox.frag 并对代码进行以下突出显示的更改:
#version 330 core 
in vec3 TexCoords; 
out vec4 color; 
uniform samplerCube skybox; 
void main() 
{ 
    color = texture(skybox, TexCoords); 
} 

将这些更改保存到你的着色器中。

现在我们已经修改了着色器文件以实现天空盒,我们将继续修改 main.cpp 文件并创建我们的天空盒。

对 main.cpp 文件的修改

main.cpp 文件中,我们需要做一些修改。遵循以下步骤:

  1. 首先,我们需要创建一个新的着色器对象,因此在我们定义 GLfloat cubeVertices[] 之前,我们需要添加 Shader skyboxShader()。我们将向其中传递着色器文件的路径:"res/shaders/skybox.vs""res/shaders/skybox.frag"

  2. 接下来,我们需要为天空盒添加更多的顶点。幸运的是,你可以参考位于 advanced_opengl 文件夹内的 main.cpp 文件中的那些顶点。将这些顶点添加到我们的代码中。

  3. 一旦你设置了天空盒的顶点,你将需要为天空盒创建一个顶点数组对象和顶点缓冲区对象。所以,让我们现在就做吧。

  4. 在我们定义了 glBindVertexArray(0) 之后,我们将添加 GLuint skyboxVAOskyboxVBO;

  5. 然后,我们将添加 glGenVertexArrays();,顶点数组将接受参数 1,然后是一个天空盒顶点数组对象,skyboxVAO。接下来,我们将生成缓冲区到天空盒顶点缓冲对象中。

  6. 因此,我们将添加 glGenBuffers(); 并传递参数 1&skyboxVBO

  7. 然后添加 glBindVertexArray(),我们将传递 skyboxVAO 给它。

  8. 接下来,我们添加 glBindBuffer(),我们将传递 GL_ARRAY_BUFFERskyboxVBO。这与我们在前几章中已经做过的非常相似,所以所有这些都应该非常熟悉。

  9. 添加 glBufferData(),这里它将接受的第一个参数是 GL_ARRAY_BUFFER,以及天空盒顶点数组的尺寸。接下来,我们需要实际传递 skyboxVertices,最后,我们将它设置为 GL_STATIC_DRAW

  10. 然后,我们将添加 GLEnableVertexAttribArray()。我们将将其设置为 0。接下来,我们将添加 glVertexAttribPointer()。这将接受 03GL_FLOATGL_FALSE3 * sizeof( GLfloat)( GLvoid * ) 0。请查看以下代码以了解描述:

// Setup skybox VAO 

    GLuint skyboxVAO, skyboxVBO; 
    glGenVertexArrays( 1, &skyboxVAO ); 
    glGenBuffers( 1, &skyboxVBO ); 
    glBindVertexArray( skyboxVAO ); 
    glBindBuffer( GL_ARRAY_BUFFER, skyboxVBO ); 
    glBufferData( GL_ARRAY_BUFFER, sizeof( skyboxVertices ),
    &skyboxVertices, GL_STATIC_DRAW ); 
    glEnableVertexAttribArray( 0 ); 
    glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(
    GLfloat ), ( GLvoid * ) 0 ); 
    glBindVertexArray(0);  

创建 Texture.h 文件

接下来,我们将实际加载纹理,所以我们将创建一个单独的纹理文件,我们将有一个用于加载纹理的方法,还有一个用于加载立方体纹理的单独方法。这样做的原因是我们将经常使用此代码,而且我们每次都必须重写这些代码。如果我们想处理多个对象,尤其是,我们不希望每次都重写这些代码。让我们看看以下步骤来创建 Texture.h 文件:

  1. 首先,我们将创建一个空的头文件,并将其命名为 Texture.h,然后将其添加到我们的项目中。

  2. 然后,在 Texture.h 中,我们将添加以下代码:

#pragma once 
  1. 然后,我们将添加一些头文件,例如 #define GLEW_STATIC(如果你没有静态链接 GLEW,那么你不需要在这里放置此行),#include <GL/glew.h>#include <vector>

  2. 接下来,我们将创建一个名为 TextureLoading 的类,并将所有代码添加到其中。

  3. 我们将输入 public,我们将拥有的第一个方法是 static GLuint LoadTexture(),我们将传递 GLchar *path 给它。

  4. 现在,我们将转到我们的 main.cpp 文件,并将所有加载和创建纹理以及纹理加载相关的代码剪切并粘贴到我们在上一步创建的 LoadTextureMethod 中。

  5. 现在,让我们看看这里需要更改的内容;查看以下突出显示的代码以了解更改:

static GLuint LoadTexture( GLchar *path ) 
    { 
        //Generate texture ID and load texture data 
        GLuint textureID; 
        glGenTextures( 1, &textureID );   
        int imageWidth, imageHeight; 
        unsigned char *image = SOIL_load_image( path, &imageWidth, 
        &imageHeight, 0, SOIL_LOAD_RGB ); 

        // Assign texture to ID 
        glBindTexture( GL_TEXTURE_2D, textureID ); 
        glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, imageWidth,
        imageHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image ); 
        glGenerateMipmap( GL_TEXTURE_2D );  

        // Parameters 
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, 
        GL_REPEAT ); 
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,
        GL_REPEAT ); 
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
        GL_LINEAR_MIPMAP_LINEAR ); 
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
        GL_LINEAR ); 
        glBindTexture( GL_TEXTURE_2D,  0); 
        SOIL_free_image_data( image );          
        return textureID; 
    } 

    static GLuint LoadCubemap( vector<const GLchar * > faces) 
    { 
        GLuint textureID; 
        glGenTextures( 1, &textureID );  
        int imageWidth, imageHeight; 
        unsigned char *image; 
            glBindTexture( GL_TEXTURE_CUBE_MAP, textureID ); 

      for ( GLuint i = 0; i < faces.size( ); i++ ) 
        { 
            image = SOIL_load_image( faces[i], &imageWidth,
            &imageHeight, 0, SOIL_LOAD_RGB ); 
            glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0,
            GL_RGB, imageWidth, imageHeight, 0, GL_RGB,
            GL_UNSIGNED_BYTE, image ); 
            SOIL_free_image_data( image ); 
        } 

glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); 
glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); 
glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); 
glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); 
glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE ); 
glBindTexture( GL_TEXTURE_CUBE_MAP, 0); 
return textureID; 

} 
  1. 现在,我们回到 main.cpp,添加 #include Texture.h,然后来到代码中我们想要加载纹理的位置,在那里我们将添加以下代码来加载我们的纹理:GLuint cubeTexture = TextureLoading::LoadTexture( "res/images/container2.png" ),并且更新绑定的纹理代码,如这里所示:
glBindTexture( GL_TEXTURE_2D, cubeTexture );   

现在,让我们运行它并检查我们的 Texture.h 代码是否成功构建,并且编译时没有错误。你应该在屏幕上看到以下输出:

图片

目前这还不是立方体贴图,因为我们只是整理了纹理,但创建一个单独的纹理将使我们能够轻松地重用纹理加载。

将立方体贴图代码添加到 Texture.h

因此,我们现在实际上想要做的过程基本上与我们处理纹理文件时的过程相似,但针对立方体贴图。代码将非常相似,所以首先我们要做的是复制纹理加载代码并将其粘贴在下面。然后,我们将对代码进行以下突出显示的更改:

static GLuint LoadCubemap( vector<const GLchar * > faces) 
    { 
        GLuint textureID; 
        glGenTextures( 1, &textureID );      
        int imageWidth, imageHeight; 
        unsigned char *image;
        glBindTexture( GL_TEXTURE_CUBE_MAP, textureID ); 
        for ( GLuint i = 0; i < faces.size( ); i++ ) 
        { 
            image = SOIL_load_image( faces[i], &imageWidth, &imageHeight,
            0, SOIL_LOAD_RGB );  
            glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB,
            imageWidth, imageHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image ); 
            SOIL_free_image_data( image ); 
        } 

        glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER,
        GL_LINEAR ); 

        glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER,
        GL_LINEAR ); 

        glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S,
        GL_CLAMP_TO_EDGE ); 

        glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T,
        GL_CLAMP_TO_EDGE ); 

        glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R,
        GL_CLAMP_TO_EDGE ); 

        glBindTexture( GL_TEXTURE_CUBE_MAP, 0); 
        return textureID; 
    }

在前面的代码中,我们添加了 GLchars,因为我们没有一条路径;记住,我们将会拥有六条不同的路径。然后,我们创建了 for 循环,因为我们想要轻松地遍历我们的六张不同图像,而且我们也不想重复代码,这正是我们这样做的主要目的。

因此,如果我们回到我们的主文件,它位于我们的 main.cpp 中,我们实际上可以完成我们正在做的事情。转到我们加载纹理文件的段落,并在该代码之后添加以下突出显示的代码:

// Cubemap (Skybox) 

    vector<const GLchar*> faces; 
    faces.push_back( "res/images/skybox/right.tga" ); 
    faces.push_back( "res/images/skybox/left.tga" ); 
    faces.push_back( "res/images/skybox/top.tga" ); 
    faces.push_back( "res/images/skybox/bottom.tga" ); 
    faces.push_back( "res/images/skybox/back.tga" ); 
    faces.push_back( "res/images/skybox/front.tga" ); 
    GLuint cubemapTexture = TextureLoading::LoadCubemap( faces ) 

在前面的代码中,我们添加了立方体贴图纹理。这里的顺序很重要,所以你不能随意放置它。如果你从网站上下载了其他图像,你可能需要正确地重新排列它。

绘制天空盒

现在,我们实际上需要做的是,嗯,绘制天空盒,所以让我们开始按照这里显示的步骤进行:

  1. 前往我们的代码中处理完所有模型矩阵的地方,我们将添加 glDepthFunc();在那里,我们需要传递 GL_LEQUAL。这改变了深度函数,所以当值等于深度缓冲区的内容时,深度测试通过。

  2. 接下来,我们将添加 skyboxShader.Use()

  3. 然后,添加 view = glmm::mat4()。在这里,我们将传递 glm::mat3(),并且我们将传递 camera.GetViewMatrix()

  4. 接下来,添加 glUniformMatrix4fv()。为此,我们将传递以下内容:

    glGetUniformLocation( skyboxShader.Program, "view" ), 1, GL_FALSE, glm::value_ptr( view )

  5. 我们还需要对投影矩阵做类似的事情。所以,我们将添加以下代码:glUniformMatrix4fv( glGetUniformLocation( skyboxShader.Program, "projection" ), 1, GL_FALSE, glm::value_ptr( projection ) );

  6. 现在我们需要做的是添加天空盒立方体。所以,添加 glBindVertexArray(); 函数,并将 skyboxVAO 传递给它,然后添加 glBindTexture()。对于绑定纹理函数,它将是 GL_TEXTURE_CUBE_MAP。然后,添加我们通过 LoadCubemap 方法调用的 cubemapTexture

  7. 然后,添加 glDrawArrays();。我们将传递的参数如下:GL_TRIANGLES036。再次强调,这只是一个立方体,所以这只是一个非常简单的事情。

  8. 接下来,添加glBindVertexArray()。为此,传递0,就像我们通常做的那样。

  9. 现在,我们只需要将glDepthFunc()设置回默认值,我们将传递GL_LESS;这仅仅是将它设置回默认状态。

现在,我们应该准备好运行了,让我们运行这个程序并检查屏幕上显示的输出。我们应该看到以下场景:

图片

它确实看起来我们已经创建了一个 3D 世界。如果你尝试远离,你会看到立方体变得越来越小。但是,其他一切保持不变,因为我们将会无限远地远离所有侧面。

摘要

在本章中,我们使用立方体贴图生成了一个 Skybox,并学习了如何将其应用于各种纹理。我们还学习了如何在代码中创建单独的纹理文件来加载我们的纹理。此外,我们还学习了如何绘制 Skybox,并使用它创建我们的游戏世界。

在以下链接中有一个关于模型加载的附加章节:www.packtpub.com/sites/default/files/downloads/ModelLoading.pdf

在本章中,你将学习如何在 Windows 上使用 CMake 设置 Assimp(Open Asset Import Library),以满足我们所有的模型加载需求。你还将学习如何创建网格类和模型类来处理我们的模型加载。

posted @ 2025-10-23 15:12  绝不原创的飞龙  阅读(26)  评论(0)    收藏  举报