LearnOpenGL - Introduction

相关链接:



一、简介

由于OpenGL是一个图形API,并不是一个独立的平台,它需要一个编程语言来工作,在这里我们使用的是C++。所以,对C++的熟练掌握在学习这个教程中是必不可少的。当然,我仍将尝试解释大部分用到的概念,包括一些高级的C++话题,所以,你并不一定要是一个C++专家才能来学习。不过,请确保你至少应该能写个比 ‘Hello World’ 复杂的程序。如果你对C++不是很熟悉,我推荐您学习一下www.learncpp.com上的免费教程。

除此之外,我们也将用到一些数学知识(线性代数、几何、三角学),同样我也会尝试解释所有的必备的数学概念。但是,毕竟我不是一个数学家,即使我的解释可能会很容易理解,但是这些解释都是不全面的。所以,在必须的时候我会链接一些不错的资料,它们会将这些概念解释的更加全面。不要被必须的数学知识吓到了,几乎所有的概念只要有基础的数学背景都可以理解。我也会将数学的内容压缩至极限。大部分的功能甚至都不需要你理解所有的数学知识,只要你会使用就行。


二、OpenGL

OpenGL到底是什么。一般它被认为是一个API(Application Programming Interface, 应用程序编程接口),包含了一系列可以操作图形、图像的函数。然而,OpenGL本身并不是一个API,它仅仅是一个由Khronos组织制定并维护的规范(Specification)。

OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的,将由OpenGL库的开发者(开发者是指编写OpenGL库的人)自行决定。因为OpenGL规范并没有规定实现的细节,具体的OpenGL库允许使用不同的实现,只要其功能和结果与规范相匹配(亦即,作为用户不会感受到功能上的差异)。

实际的OpenGL库的开发者通常是显卡的生产商。你购买的显卡所支持的OpenGL版本都为这个系列的显卡专门开发的。当你使用Apple系统的时候,OpenGL库是由Apple自身维护的。在Linux下,有显卡生产商提供的OpenGL库,也有一些爱好者改编的版本。这也意味着任何时候OpenGL库表现的行为与规范规定的不一致时,基本都是库的开发者留下的bug。

由于OpenGL的大多数实现都是由显卡厂商编写的,当产生一个bug时通常可以通过升级显卡驱动来解决。这些驱动会包括你的显卡能支持的最新版本的OpenGL,这也是为什么总是建议你偶尔更新一下显卡驱动。

所有版本的OpenGL规范文档都被公开的寄存在Khronos那里。有兴趣的读者可以找到OpenGL3.3(后续将要使用的版本)的规范文档。如果你想深入到OpenGL的细节(只关心函数功能的描述而不是函数的实现),这是个很好的选择。如果你想知道每个函数具体的运作方式,这个规范也是一个很棒的参考。

基元类型(Primitive Type)
使用OpenGL时,建议使用OpenGL定义的基元类型。比如使用float时我们加上前缀GL(因此写作GLfloat)。int、uint、char、bool等等也类似。OpenGL定义的这些GL基元类型的内存布局是与平台无关的,而int等基元类型在不同操作系统上可能有不同的内存布局。使用GL基元类型可以保证你的程序在不同的平台上工作一致。


2.1. 核心模式与立即渲染模式

早期的OpenGL使用立即渲染模式Immediate mode,也就是固定渲染管线),这个模式下绘制图形很方便。OpenGL的大多数功能都被库隐藏起来,开发者很少有控制OpenGL如何进行计算的自由。而开发者迫切希望能有更多的灵活性。随着时间推移,规范越来越灵活,开发者对绘图细节有了更多的掌控。立即渲染模式确实容易使用和理解,但是效率太低。因此从OpenGL3.2开始,规范文档开始废弃立即渲染模式,并鼓励开发者在OpenGL的核心模式(Core-profile)下进行开发,这个分支的规范完全移除了旧的特性。

当使用OpenGL的核心模式时,OpenGL迫使我们使用现代的函数。当我们试图使用一个已废弃的函数时,OpenGL会抛出一个错误并终止绘图。现代函数的优势是更高的灵活性和效率,然而也更难于学习。立即渲染模式从OpenGL实际运作中抽象掉了很多细节,因此它在易于学习的同时,也很难让人去把握OpenGL具体是如何运作的。现代函数要求使用者真正理解OpenGL和图形编程,它有一些难度,然而提供了更多的灵活性,更高的效率,更重要的是可以更深入的理解图形编程。

这也是为什么我们用面向OpenGL3.3的核心模式。

现今,更高版本的OpenGL已经发布(写作时最新版本为4.5),你可能会问:既然OpenGL 4.5 都出来了,为什么我们还要学习OpenGL 3.3?答案很简单,所有OpenGL的更高的版本都是在3.3的基础上,引入了额外的功能,并没有改动核心架构。新版本只是引入了一些更有效率或更有用的方式去完成同样的功能。因此,所有的概念和技术在现代OpenGL版本里都保持一致。当你的经验足够,你可以轻松使用来自更高版本OpenGL的新特性。

当使用新版本的OpenGL特性时,只有新一代的显卡能够支持你的应用程序。
这也是为什么大多数开发者基于较低版本的OpenGL编写程序,并只提供选项启用新版本的特性。

2.2. 扩展

OpenGL的一大特性就是对扩展(Extension)的支持,当一个显卡公司提出一个新特性或者渲染上的大优化,通常会以扩展的方式在驱动中实现。如果一个程序在支持这个扩展的显卡上运行,开发者可以使用这个扩展提供的一些更先进更有效的图形功能。通过这种方式,开发者不必等待一个新的OpenGL规范面世,就可以使用这些新的渲染特性了,只需要简单地检查一下显卡是否支持此扩展。通常,当一个扩展非常流行或者非常有用的时候,它将最终成为未来的OpenGL规范的一部分。

使用扩展的代码大多看上去如下:

if(GL_ARB_extension_name)
{
    // 使用硬件支持的全新的现代特性
}
else
{
    // 不支持此扩展: 用旧的方式去做
}

使用OpenGL3.3时,我们很少需要使用扩展来完成大多数功能。


2.3. 状态机

OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。我们通常使用如下途径去更改OpenGL状态:设置选项,操作缓冲。最后,我们使用当前OpenGL上下文来渲染。

假设当我们想告诉OpenGL去画线段而不是三角形的时候,我们通过改变一些上下文变量来改变OpenGL状态,从而告诉OpenGL如何去绘图。一旦我们改变了OpenGL的状态为绘制线段,下一个绘制命令就会画出线段而不是三角形。

当使用OpenGL的时候,我们会遇到一些状态设置函数(State-changing Function),这类函数将会改变上下文。以及状态使用函数(State-using Function),这类函数会根据当前OpenGL的状态执行一些操作。只要你记住OpenGL本质上是个大状态机,就能更容易理解它的大部分特性。


2.4. 对象

OpenGL库是用C语言写的,同时也支持多种语言的派生,但其内核仍是一个C库。由于C的一些语言结构不易被翻译到其它的高级语言,因此OpenGL开发的时候引入了一些抽象层。“对象(Object)”就是其中一个。

在OpenGL中一个对象是指一些选项的集合,它代表OpenGL状态的一个子集。比如,我们可以用一个对象来代表绘图窗口的设置,之后我们就可以设置它的大小、支持的颜色位数等等。可以把对象看做一个C风格的结构体(Struct):

struct object_name 
{
    float  option1;
    int    option2;
    char[] name;
};

请记住,使用OpenGL的类型的好处是保证了在各平台中每一种类型的大小都是统一的。
你也可以使用其它的定宽类型(Fixed-width Type)来实现这一点。


当我们使用一个对象时,通常看起来像如下一样(把OpenGL上下文看作一个大的结构体):

// OpenGL的状态
struct OpenGL_Context 
{
    ...
    object* object_Window_Target;
    ...     
};
// 创建对象
unsigned int objectId = 0;
glGenObject(1, &objectId);
// 绑定对象至上下文
glBindObject(GL_WINDOW_TARGET, objectId);
// 设置当前绑定到 GL_WINDOW_TARGET 的对象的一些选项
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);
// 将上下文对象设回默认
glBindObject(GL_WINDOW_TARGET, 0);

这一小段代码展现了你以后使用OpenGL时常见的工作流
我们首先创建一个对象,然后用一个id保存它的引用(实际数据被储存在后台)。
然后我们将对象绑定至上下文的目标位置(例子中窗口对象目标的位置被定义成GL_WINDOW_TARGET)。
接下来我们设置窗口的选项。
最后我们将目标位置的对象id设回0,解绑这个对象。
设置的选项将被保存在objectId所引用的对象中,一旦我们重新绑定这个对象到GL_WINDOW_TARGET位置,这些选项就会重新生效。

示例代码只是OpenGL如何操作的一个大致描述。

使用对象的一个好处是在程序中,我们不止可以定义一个对象,并设置它们的选项,每个对象都可以是不同的设置。在我们执行一个使用OpenGL状态的操作的时候,只需要绑定含有需要的设置的对象即可。比如说我们有一些作为3D模型数据(一栋房子或一个人物)的容器对象,在我们想绘制其中任何一个模型的时候,只需绑定一个包含对应模型数据的对象就可以了(当然,我们需要先创建并设置对象的选项)。拥有数个这样的对象允许我们指定多个模型,在想画其中任何一个的时候,直接将对应的对象绑定上去,便不需要再重复设置选项了。


三、创建窗口

在我们画出出色的效果之前,首先要做的就是创建一个OpenGL上下文(Context)和一个用于显示的窗口。然而,这些操作在每个系统上都是不一样的,OpenGL有意将这些操作抽象(Abstract)出去。这意味着我们不得不自己处理创建窗口,定义OpenGL上下文以及处理用户输入。

幸运的是,有一些库已经提供了我们所需的功能,其中一部分是特别针对OpenGL的。这些库节省了我们书写操作系统相关代码的时间,提供给我们一个窗口一个OpenGL上下文用来渲染。最流行的几个库有GLUTSDLSFMLGLFW。下文将使用GLFW。你可以随意选用其他类似的库,大多数库的配置方法和GLFW差不多。


3.1. 构建GLFW

GLFW是一个专门针对OpenGL的C语言库,它提供了一些渲染物体所需的最低限度的接口。它允许用户创建OpenGL上下文、定义窗口参数以及处理用户输入,对我们来说这就够了。

后续配置链接相关,我使用的是Microsoft Visual Studio 2019 IDE(操作过程在更新的Visual Studio都是相同的)。如果你用的不是Visual Studio(或者用的是它的旧版本)请不要担心,大多数IDE上的操作都是类似的。

GLFW可以从它官方网站的下载页上获取。GLFW已提供为Visual Studio(2012到2019都有)预编译好的二进制版本和相应的头文件,但是为了完整性我们将从编译源代码开始。所以我们需要下载源代码包。
在这里插入图片描述
下载源码包之后,将其解压并打开。我们只需要里面的这些内容:

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

在这里插入图片描述
从源代码编译库可以保证生成的库完全适合你的操作系统和CPU的,而预编译的二进制文件则并非总是提供(有时候,即便提供了预编译的二进制文件,也可能不适用于您的系统)。开放源代码所产生问题在于:并不是每个人都用相同的IDE或者构建系统来搞开发,因而提供的项目/解决方案文件可能和一些人的IDE不兼容。所以人们必须使用给定的.c/.cpp和.h/.hpp文件来自己建立项目/解决方案,这是一项很枯燥的工作。但因此也诞生了一个叫做CMake的工具。


3.2. CMake工具

CMake是一个工程文件生成工具。用户可以使用预定义好的CMake脚本,根据自己的选择(像是Visual Studio, Code::Blocks, Eclipse)生成不同IDE的工程文件。这允许我们从GLFW源码创建一个Visual Studio 2019工程文件,之后进行编译。首先,我们需要从这里 下载 安装CMake。
在这里插入图片描述

当CMake安装成功后,你可以选择从命令行或者GUI启动CMake,由于我们不想让事情变得太过复杂,我们选择用GUI。CMake需要一个源代码目录和一个存放编译结果的目标文件目录。源代码目录我们选择GLFW的源代码的根目录,然后我们新建一个 build 文件夹,选中作为目标目录。
在这里插入图片描述
在设置完源代码目录和目标目录之后,点击Configure(设置)按钮,让CMake读取设置和源代码。我们接下来需要选择工程的生成器,由于我们使用的是Visual Studio 2019,我们选择 Visual Studio 16 选项(因为Visual Studio 2019的内部版本号是16)。CMake会显示可选的编译选项用来配置最终生成的库。这里我们使用默认设置,并再次点击Configure(设置)按钮保存设置。保存之后,点击Generate(生成)按钮,生成的工程文件会在你的build文件夹中。


build文件夹里可以找到GLFW.sln文件,用Visual Studio 2019打开。因为CMake已经配置好了项目,并按照默认配置将其编译为64位的库,所以我们直接点击Build Solution(生成解决方案)按钮,然后在build/src/Debug文件夹内就会出现我们编译出的库文件glfw3.lib

库生成完毕之后,我们需要让IDE知道库和头文件的位置。有两种方法:

  1. 找到IDE或者编译器的**/lib/include文件夹,添加GLFWinclude文件夹里的文件到IDE的/include文件夹里去。用类似的方法,将glfw3.lib添加到/lib**文件夹里去。虽然这样能工作,但这不是推荐的方式,因为这样会让你很难去管理库和include文件,而且重新安装IDE或编译器可能会导致这些文件丢失。

  2. 推荐的方式是建立一个新的目录包含所有的第三方库文件和头文件,并且在你的IDE或编译器中指定这些文件夹。我个人会使用一个单独的文件夹,里面包含LibsInclude文件夹,在这里存放OpenGL工程用到的所有第三方库和头文件。这样我的所有第三方库都在同一个位置(并且可以共享至多台电脑)。然而这要求你每次新建一个工程时都需要告诉IDE/编译器在哪能找到这些目录。

完成上面步骤后,就可以使用GLFW创建我们的第一个OpenGL工程了!


3.3. 编译和链接GLEW

GLEWOpenGL Extension Wrangler Library的缩写,它能解决我们上面提到的那个繁琐的问题。因为GLEW也是一个库,我们同样需要构建并将其链接进工程。GLEW可以从这里下载,你同样可以选择下载二进制版本,如果你的目标平台列在上面的话,或者下载源码编译,步骤和编译GLFW时差不多。记住,如果不确定的话,选择32位的二进制版本。

我们使用GLEW的静态版本glew32s.lib(注意这里的“s”),将库文件添加到你的库目录,将include内容添加到你的include目录。接下来,在VS的链接器选项里加上glew32s.lib。注意GLFW3(默认)也是编译成了一个静态库。

静态(Static)链接是指编译时就将库代码里的内容整合进你的二进制文件。
优点就是你不需要管理额外的文件了,只需要发布你单独的一个二进制文件就行了。
缺点就是你的可执行文件会变得更大,另外当库有升级版本时,你必须重新进行编译整个程序。

动态(Dynamic)链接是指一个库通过.dll或.so的方式存在,它的代码与你的二进制文件的代码是分离的。
优点是使你的二进制文件大小变小并且更容易升级,
缺点是你最终发布程序时必须带上这些DLL。

如果你希望静态链接GLEW,必须在包含GLEW头文件之前定义预处理器宏GLEW_STATIC

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

如果你希望动态链接,那么你可以省略这个宏。但是记住使用动态链接的话你需要拷贝一份.DLL文件到你的应用程序目录。

对于用GCC编译的Linux用户建议使用这个命令行选项-lGLEW -lglfw3 -lGL -lX11 -lpthread -lXrandr -lXi。没有正确链接相应的库会产生 undefined reference(未定义的引用) 这个错误。


四、第一个工程

首先,打开Visual Studio,创建一个新的项目。如果VS提供了多个选项,选择Visual C++,然后选择Empty Project(空项目)(别忘了给你的项目起一个合适的名字)。由于我们将在64位模式中执行所有操作,而新项目默认是32位的,因此我们需要将Debug旁边顶部的下拉列表从x86更改为x64
在这里插入图片描述

4.1. 链接

为了使我们的程序使用GLFW,我们需要把GLFW库链接(Link)进工程。这可以通过在链接器的设置里指定我们要使用glfw3.lib来完成,但是由于我们将第三方库放在另外的目录中,我们的工程还不知道在哪寻找这个文件。于是我们首先需要将我们放第三方库的目录添加进设置。

要添加这些目录(需要VS搜索库和include文件的地方),我们首先进入Project Properties(工程属性,在解决方案窗口里右键项目),然后选择VC++ Directories(VC++ 目录)选项卡(如下图)。在下面的两栏添加目录:
在这里插入图片描述
这里你可以把自己的目录加进去,让工程知道到哪去搜索。你需要手动把目录加在后面,也可以点击需要的位置字符串,选择选项,之后会出现类似下面这幅图的界面,图是选择
Include Directories(包含目录)时的界面:
在这里插入图片描述
这里可以添加任意多个目录,IDE会从这些目录里寻找头文件。所以只要你将
GLFW的Include
文件夹加进路径中,你就可以使用<GLFW/..>来引用头文件。库文件夹也是一样的。

现在VS可以找到所需的所有文件了。最后需要在Linker(链接器) 选项卡里的 Input(输入) 选项卡里添加glfw3.lib这个文件:

在这里插入图片描述

要链接一个库我们必须告诉链接器它的文件名。库名字是glfw3.lib,我们把它加到Additional Dependencies(附加依赖项)字段中(手动或者使用选项都可以)。这样GLFW在编译的时候就会被链接进来了。除了GLFW之外,你还需要添加一个链接条目链接到OpenGL的库,但是这个库可能因为系统的不同而有一些差别。

  1. 如果你是Windows平台,opengl32.lib已经包含在Microsoft SDK里了,它在Visual Studio安装的时候就默认安装了。由于上文中用的是VS编译器,并且是在Windows操作系统上,我们只需将opengl32.lib添加进链接器设置里就行了。值得注意的是,OpenGL库64位版本的文件名仍然是opengl32.lib(和32位版本一样),虽然很奇怪但确实如此。

  2. 在Linux下你需要链接libGL.so库文件,这需要添加-lGL到你的链接器设置中。如果找不到这个库你可能需要安装Mesa,NVidia或AMD的开发包,这部分因平台而异(而且我也不熟悉Linux)就不仔细讲解了。
    接下来,如果你已经添加GLFW和OpenGL库到连接器设置中,你可以用如下方式添加GLFW头文件:

#include <GLFW\glfw3.h>

对于用GCC编译的Linux用户建议
使用这个命令行选项-lglfw3 -lGL -lX11 -lpthread -lXrandr -lXi -ldl
没有正确链接相应的库会产生 undefined reference (未定义的引用) 这个错误。


4.2. GLAD

我们仍然还有一件事要做。因为OpenGL只是一个标准/规范,具体的实现是由驱动开发商针对特定显卡实现的。由于OpenGL驱动版本众多,它大多数函数的位置都无法在编译时确定下来,需要在运行时查询。所以任务就落在了开发者身上,开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用。取得地址的方法因平台而异,在Windows上会是类似这样:

// 定义函数原型
typedef void (*GL_GENBUFFERS) (GLsizei, GLuint*);
// 找到正确的函数并赋值给函数指针
GL_GENBUFFERS glGenBuffers  = (GL_GENBUFFERS)wglGetProcAddress("glGenBuffers");
// 现在函数可以被正常调用了
GLuint buffer;
glGenBuffers(1, &buffer);

你可以看到代码非常复杂,而且很繁琐,我们需要对每个可能使用的函数都要重复这个过程。幸运的是,有些库能简化此过程,其中GLAD是目前最新,也是最流行的库。


GLAD是一个开源的库,它能解决我们上面提到的那个繁琐的问题。GLAD的配置与大多数的开源库有些许的不同,GLAD使用了一个在线服务。在这里我们能够告诉GLAD需要定义的OpenGL版本,并且根据这个版本加载所有相关的OpenGL函数。

打开GLAD的在线服务,将语言(Language)设置为C/C++,在API选项中,选择3.3以上的OpenGL(gl)版本(后续将使用3.3版本,但更新的版本也能用)。之后将模式(Profile)设置为Core,并且保证选中了**生成加载器(Generate a loader)选项。现在可以先(暂时)忽略扩展(Extensions)中的内容。都选择完之后,点击生成(Generate)**按钮来生成库文件。
在这里插入图片描述

GLAD现在应该提供给你了一个zip压缩文件,包含两个头文件目录,和一个glad.c文件。将两个头文件目录(glad和KHR)复制到你的Include文件夹中(或者增加一个额外的项目指向这些目录),并添加glad.c文件到你的工程中。
在这里插入图片描述

经过前面的这些步骤之后,你就应该可以将以下的指令加到你的文件顶部了:

#include <glad/glad.h> 

点击编译按钮应该不会给你提示任何的错误。


4.3. GLEW

GLEW是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLEW。

glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK)
{
    std::cout << "Failed to initialize GLEW" << std::endl;
    return -1;
}

请注意,我们在初始化GLEW之前设置glewExperimental变量的值为GL_TRUE,这样做能让GLEW在管理OpenGL的函数指针时更多地使用现代化的技术,如果把它设置为GL_FALSE的话可能会在使用OpenGL的核心模式时出现一些问题。


4.4. 窗口

让我们试试能不能让GLFW正常工作。首先,新建一个.cpp文件,然后把下面的代码粘贴到该文件的最前面。

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

请确认是在包含GLFW的头文件之前包含了GLAD的头文件。GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h),所以需要在其它依赖于OpenGL的头文件之前包含GLAD。

接下来我们创建main函数,在这个函数中我们将会实例化GLFW窗口

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

    return 0;
}

首先,我们在main函数中调用glfwInit函数来初始化GLFW,然后我们可以使用glfwWindowHint函数来配置GLFW。glfwWindowHint函数的第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;第二个参数接受一个整型,用来设置这个选项的值。该函数的所有的选项以及对应的值都可以在 GLFW’s window handling 这篇文档中找到。如果你现在编译你的cpp文件会得到大量的 undefined reference (未定义的引用)错误,也就是说你并未顺利地链接GLFW库。

由于是基于OpenGL 3.3版本展开讨论的,所以我们需要告诉GLFW我们要使用的OpenGL版本是3.3,这样GLFW会在创建OpenGL上下文时做出适当的调整。这也可以确保用户在没有适当的OpenGL版本支持的情况下无法运行。我们将主版本号(Major)和次版本号(Minor)都设为3。我们同样明确告诉GLFW我们使用的是核心模式(Core-profile)。明确告诉GLFW我们需要使用核心模式意味着我们只能使用OpenGL功能的一个子集(没有我们已不再需要的向后兼容特性)。如果使用的是Mac OS X系统,你还需要加下面这行代码到你的初始化代码中这些配置才能起作用(将上面的代码解除注释):

glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

请确认您的系统支持OpenGL3.3或更高版本,否则此应用有可能会崩溃或者出现不可预知的错误。如果想要查看OpenGL版本的话,在Linux上运行glxinfo,或者在Windows上使用其它的工具(例如OpenGL Extension Viewer)。如果你的OpenGL版本低于3.3,检查一下显卡是否支持OpenGL 3.3+(不支持的话你的显卡真的太老了),并更新你的驱动程序,有必要的话请更新显卡。


接下来我们创建一个窗口对象,这个窗口对象存放了所有和窗口相关的数据,而且会被GLFW的其他函数频繁地用到。

GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
    std::cout << "Failed to create GLFW window" << std::endl;
    glfwTerminate();
    return -1;
}
glfwMakeContextCurrent(window);

glfwCreateWindow函数需要窗口的宽和高作为它的前两个参数。第三个参数表示这个窗口的名称(标题),这里我们使用"LearnOpenGL",当然你也可以使用你喜欢的名称。最后两个参数我们暂时忽略。这个函数将会返回一个GLFWwindow对象,我们会在其它的GLFW操作中使用到。创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了。


在之前的教程中已经提到过,GLAD是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLAD

if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
    std::cout << "Failed to initialize GLAD" << std::endl;
    return -1;
}

我们给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数。GLFW给我们的是glfwGetProcAddress,它根据我们编译的系统定义了正确的函数。


在我们开始渲染之前还有一件重要的事情要做,我们必须告诉OpenGL渲染窗口的尺寸大小,即视口(Viewport),这样OpenGL才只能知道怎样根据窗口大小显示数据和坐标。我们可以通过调用glViewport函数来设置窗口的维度(Dimension):

glViewport(0, 0, 800, 600);

glViewport函数前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素)。

我们实际上也可以将视口的维度设置为比GLFW的维度小,这样子之后所有的OpenGL渲染将会在一个更小的窗口中显示,这样子的话我们也可以将一些其它元素显示在OpenGL视口之外。

OpenGL幕后使用glViewport中定义的位置和宽高进行2D坐标的转换,将OpenGL中的位置坐标转换为你的屏幕坐标。例如,OpenGL中的坐标(-0.5, 0.5)有可能(最终)被映射为屏幕中的坐标(200,450)。注意,处理过的OpenGL坐标范围只为-1到1,因此我们事实上将(-1到1)范围内的坐标映射到(0, 800)和(0, 600)。

然而,当用户改变窗口的大小的时候,视口也应该被调整。我们可以对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用。这个回调函数的原型如下:

void framebuffer_size_callback(GLFWwindow* window, int width, int height);

这个帧缓冲大小函数需要一个GLFWwindow作为它的第一个参数,以及两个整数表示窗口的新维度。每当窗口改变大小,GLFW会调用这个函数并填充相应的参数供你处理。

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
}

我们还需要注册这个函数,告诉GLFW我们希望每当窗口调整大小的时候调用这个函数:

glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

当窗口被第一次显示的时候framebuffer_size_callback也会被调用。对于视网膜(Retina)显示屏,width和height都会明显比原输入值更高一点。

我们还可以将我们的函数注册到其它很多的回调函数中。比如说,我们可以创建一个回调函数来处理手柄输入变化,处理错误消息等。我们会在创建窗口之后,渲染循环初始化之前注册这些回调函数。


4.5. 关于渲染引擎

我们可不希望只绘制一个图像之后我们的应用程序就立即退出并关闭窗口。我们希望程序在我们主动关闭它之前不断绘制图像并能够接受用户输入。因此,我们需要在程序中添加一个while循环,我们可以把它称之为渲染循环(Render Loop),它能在我们让GLFW退出前一直保持运行。下面几行的代码就实现了一个简单的渲染循环:

while(!glfwWindowShouldClose(window))
{
    glfwSwapBuffers(window);
    glfwPollEvents();    
}
  • glfwWindowShouldClose函数在我们每次循环的开始前检查一次GLFW是否被要求退出,如果是的话该函数返回true然后渲染循环便结束了,之后为我们就可以关闭应用程序了。
  • glfwPollEvents函数检查有没有触发什么事件(比如键盘输入、鼠标移动等)、更新窗口状态,并调用对应的回调函数(可以通过回调方法手动设置)。
  • glfwSwapBuffers函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲),它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上。

双缓冲(Double Buffer)
应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从>左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染
的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。


当渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。我们可以在main函数的最后调用glfwTerminate函数来完成。

glfwTerminate();
return 0;

这样便能清理所有的资源并正确地退出应用程序。现在你可以尝试编译并运行你的应用程序了,如果没做错的话,你将会看到如下的输出:
在这里插入图片描述

如果你看见了一个非常无聊的黑色窗口,那么就对了!如果你没得到正确的结果,或者你不知道怎么把所有东西放到一起,请参考源代码。

如果程序编译有问题,请先检查连接器选项是否正确,IDE中是否导入了正确的目录。并且请确认你的代码是否正确,直接对照上面提供的源代码就行了。


我们同样也希望能够在GLFW中实现一些输入控制,这可以通过使用GLFW的几个输入函数来完成。我们将会使用GLFW的glfwGetKey函数,它需要一个窗口以及一个按键作为输入。这个函数将会返回这个按键是否正在被按下。我们将创建一个processInput函数来让所有的输入代码保持整洁。

void processInput(GLFWwindow *window)
{
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

这里我们检查用户是否按下了返回键(Esc)(如果没有按下,glfwGetKey将会返回GLFW_RELEASE。如果用户的确按下了返回键,我们将通过glfwSetwindowShouldClose使用把WindowShouldClose属性设置为 true的方法关闭GLFW。下一次while循环的条件检测将会失败,程序将会关闭。

我们接下来在渲染循环的每一个迭代中调用processInput

while (!glfwWindowShouldClose(window))
{
    processInput(window);

    glfwSwapBuffers(window);
    glfwPollEvents();
}

这就给我们一个非常简单的方式来检测特定的键是否被按下,并在每一帧做出处理。


渲染
我们要把所有的 渲染(Rendering) 操作放到渲染循环中,因为我们想让这些渲染指令在每次渲染循环迭代的时候都能被执行。代码将会是这样的:

// 渲染循环
while(!glfwWindowShouldClose(window))
{
    // 输入
    processInput(window);

    // 渲染指令
    ...

    // 检查并调用事件,交换缓冲
    glfwPollEvents();
    glfwSwapBuffers(window);
}

为了测试一切都正常工作,我们使用一个自定义的颜色清空屏幕。在每个新的渲染迭代开始的时候我们总是希望清屏,否则我们仍能看见上一次迭代的渲染结果(这可能是你想要的效果,但通常这不是)。我们可以通过调用glClear函数来清空屏幕的颜色缓冲,它接受一个**缓冲位(Buffer Bit)**来指定要清空的缓冲,可能的缓冲位有GL_COLOR_BUFFER_BITGL_DEPTH_BUFFER_BITGL_STENCIL_BUFFER_BIT。由于现在我们只关心颜色值,所以我们只清空颜色缓冲。

glClearColor(1.0f, 0.8f, 0.8f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

注意,除了glClear之外,我们还调用了glClearColor来设置清空屏幕所用的颜色。当调用glClear函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。在这里,我们将屏幕设置为了深蓝色。

glClearColor函数是一个状态设置函数,
glClear函数则是一个状态使用的函数,它使用了当前的状态来获取应该清除为的颜色。

在这里插入图片描述


相关资源与链接:

  • GLFW: Window Guide:GLFW官方的配置GLFW窗口的指南。
  • Building applications:提供了很多编译或链接相关的信息和一大列错误及对应的解决方案。
  • GLFW with Code::Blocks:使用Code::Blocks IDE编译GLFW。
  • Running CMake:简要的介绍如何在Windows和Linux上使用CMake。
  • Writing a build system under Linux:Wouter Verholst写的一个autotools的教程,讲的是如何在Linux上编写构建系统,尤其是针对这些教程。
  • Polytonic/Glitter:一个简单的样板项目,它已经提前配置了所有相关的库;如果你想要很方便地搞到一个LearnOpenGL教程的范例工程,这也是很不错的。

源代码

1. 没有颜色缓冲的源代码:

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

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;


int main()
{
	// glfw: initialize and configure
	// ------------------------------
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

#ifdef __APPLE__
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

	// glfw window creation
    // --------------------
	//创建一个窗口对象,这个窗口对象存放了所有和窗口相关的数据
	GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
	if(window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	// glad: load all OpenGL function pointers
	// ---------------------------------------
	//初始化GLAD,管理OpenGL的函数指针
	if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	// render loop
	// -----------
	//实现简单的渲染引擎
	while(!glfwWindowShouldClose(window))
	{
		// input
		// -----
		processInput(window);

		// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
		// -------------------------------------------------------------------------------
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
	// glfw: terminate, clearing all previously allocated GLFW resources.
	// ------------------------------------------------------------------
	//正确释放/删除之前的分配的所有资源
	glfwTerminate();
	return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
	if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
		glfwSetWindowShouldClose(window, true);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	// make sure the viewport matches the new window dimensions; note that width and 
	// height will be significantly larger than specified on retina displays.
	glViewport(0, 0, width, height);
}

2. 有颜色缓冲的源代码:

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

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;


int main()
{
	// glfw: initialize and configure
	// ------------------------------
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

#ifdef __APPLE__
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

	// glfw window creation
    // --------------------
	//创建一个窗口对象,这个窗口对象存放了所有和窗口相关的数据
	GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
	if(window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	// glad: load all OpenGL function pointers
	// ---------------------------------------
	//初始化GLAD,管理OpenGL的函数指针
	if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	// render loop
	// -----------
	//实现简单的渲染引擎
	while(!glfwWindowShouldClose(window))
	{
		// input
		// -----
		processInput(window);

		// render
		// ------
		glClearColor(1.0f, 0.8f, 0.8f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
		// -------------------------------------------------------------------------------
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
	// glfw: terminate, clearing all previously allocated GLFW resources.
	// ------------------------------------------------------------------
	//正确释放/删除之前的分配的所有资源
	glfwTerminate();
	return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
	if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
		glfwSetWindowShouldClose(window, true);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	// make sure the viewport matches the new window dimensions; note that width and 
	// height will be significantly larger than specified on retina displays.
	glViewport(0, 0, width, height);
}

二者的主要区别是
在这里插入图片描述


附:配置GLAD环境时出现无法解析外部符号问题的解决

在配置环境后,运行调用窗口代码,出现以下错误:
错误 LNK2019 无法解析的外部符号 _gladLoadGLLoader,函数 _main 中引用了该符号 OpenGLTest;错误 LNK2001 无法解析的外部符号 _glad_debug_glViewport;
在这里插入图片描述

解决办法:把工程文件中的glad.c通过添加现有项加入解决方案。
在这里插入图片描述


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值