mirror of
https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git
synced 2025-08-23 12:45:29 +08:00
Mkdocs test
Hello Triangle is not completed
This commit is contained in:
118
01 Getting started/04 Hello Triangle.md
Normal file
118
01 Getting started/04 Hello Triangle.md
Normal file
@@ -0,0 +1,118 @@
|
||||
本文作者JoeyDeVries,由[Django](http://bullteacher.com/5-hello-triangle.html)翻译自[http://learnopengl.com](http://www.learnopengl.com/#!Getting-started/Hello-Triangle)
|
||||
|
||||
# Hello Triangle
|
||||
|
||||
在OpenGL中任何事物都在3D空间中,但是屏幕和窗口是一个2D像素阵列,所以OpenGL的大部分工作都是关于如何把3D坐标转变为适应你的屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的**图形输送管道**(pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形输送管道可以被划分为两个主要部分:第一个部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。这个教程里,我们会简单地讨论一下图形输送管道,以及如何使用它创建一些像素,这对我们来说是有好处的。
|
||||
|
||||
<div style="border:solid #AFDFAF;border-radius:10px;background-color:#D8F5D8;margin:10px 10px 10px 0px;padding:10px">
|
||||
2D坐标和像素也是不同的,2D坐标是在2D空间中的一个点的非常精确的表达,2D像素是这个点的近似,它受到你的屏幕/窗口解析度的限制。
|
||||
</div>
|
||||
|
||||
图形输送管道接收一组3D坐标,然后把它们转变为你屏幕上的有色2D像素。图形输送管道可以被划分为几个阶段,每个阶段需要把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们有一个特定的函数),它们能简单地并行执行。由于它们的并行执行的特征,当今大多数显卡都有成千上万的小处理核心,在GPU上为每一个(输送管道的)阶段运行各自的小程序,从而在图形输送管道中快速处理你的数据。这些小程序叫做**着色器**(Shader)。
|
||||
|
||||
有些着色器允许开发者自己配置,我们可以用自己写的着色器替换默认的。这样我们就可以更细致地控制输送管道的特定的部分了,因为它们运行在GPU上,它们也会节约宝贵的CPU时间。着色器是用**OpenGL着色器语言**(OpenGL Shading Language, GLSL)写成的,我们在下节会花更多时间研究它。
|
||||
|
||||
在下面,你会看到一个图形输送管道的每个阶段的抽象表达。要注意蓝色部分代表的是我们可以自定义的着色器。
|
||||
|
||||

|
||||
|
||||
如你所见,图形输入管道包含很多部分,每个都是将你的顶点数据转变为最后渲染出来的像素这个大过程中的一个特定阶段。我们会概括性地解释输送管道的每个部分,从而使你对输送管道的工作方式有个大概了解。
|
||||
|
||||
我们以数组的形式传递3个3D坐标作为图形输送管道的输入,它用来表示一个三角形,这个数组叫做顶点数据(Vertex Data);这里顶点数据是一些顶点的集合。一个**顶点**是一个3D坐标的集合(也就是x、y、z数据)。而顶点数据是用**顶点属性**(vertex attributes)表示的,它可以包含任何我们希望用的数据,但是简单起见,我们还是假定每个顶点只由一个3D位置①和几个颜色值组成的吧。
|
||||
|
||||
①译注:当我们谈论一个“位置”的时候,它代表在一个“空间”中所处地点的这个特殊属性;同时“空间”代表着任何一种坐标系,比如x、y、z三维坐标系,x、y二维坐标系,或者一条直线上的x和y的线性关系,只不过二维坐标系是一个扁扁的平面空间,而一条直线是一个很瘦的长长的空间。
|
||||
|
||||
<div style="border:solid #AFDFAF;border-radius:10px;background-color:#D8F5D8;margin:10px 10px 10px 0px;padding:10px">
|
||||
为了让OpenGL知道我们的坐标和颜色值构成的到底是什么,OpenGL需要你去提示你希望这些数据所表示的是什么类型。我们是希望把这些数据渲染成一系列的点?一系列的三角形?还是仅仅是一个长长的线?做出的这些提示叫做**基本图形**(Primitives),任何一个绘制命令的调用都必须把基本图形类型传递给OpenGL。这是其中的几个:`GL_POINTS`、`GL_TRIANGLES`、`GL_LINE_STRIP`。
|
||||
</div>
|
||||
|
||||
输送管道的第一个部分是**顶点着色器**(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释),同时顶点着色器允许我们对顶点属性进行一些基本处理。
|
||||
|
||||
**基本图形组装**(Primitive Assembly)阶段把顶点着色器的表示为基本图形的所有顶点作为输入(如果选择的是`GL_POINTS`,那么就是一个单独顶点),把所有点组装为特定的基本图形的形状;本节例子是一个三角形。
|
||||
|
||||
基本图形组装阶段的输出会传递给**几何着色器**(Geometry Shader)。几何着色器把基本图形形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其他的)基本图形来生成其他形状。例子中,它生成了另一个三角形。
|
||||
|
||||
**细分着色器**(Tessellation Shaders)拥有把给定基本图形**细分**为更多小基本图形的能力。这样我们就能在物体更接近玩家的时候通过创建更多的三角形的方式创建出更加平滑的视觉效果。
|
||||
|
||||
细分着色器的输出会进入**像素化**(Rasterization也译为光栅化)阶段,这里它会把基本图形映射为屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)②。在片段着色器运行之前,会执行**裁切**(clipping)。裁切会丢弃超出你的视图以外的那些像素,来提升执行效率。
|
||||
|
||||
②译注:Fragment通常译为片段,但从根本上来说它就是带有一些额外信息的像素,由于带有额外信息,OpenGL就没有给它取名叫“像素”,所以“片段”的中文译法比较不准确听起来就像一个fragment可能包含多个像素一样,实际上一个fragment只包含一个像素,现在你只要简单记住它是像素就行了。后面你会知道除了像素信息以外它还包含了什么。
|
||||
|
||||
<div style="border:solid #AFDFAF;border-radius:10px;background-color:#D8F5D8;margin:10px 10px 10px 0px;padding:10px">
|
||||
OpenGL中的一个fragment是OpenGL渲染一个独立像素所需的所有数据。
|
||||
</div>
|
||||
|
||||
**片段着色器**的主要目的是计算一个像素的最终颜色,这也是OpenGL高级效果产生的地方。通常,片段着色器包含用来计算像素最终颜色的3D场景的一些数据(比如光照、阴影、光的颜色等等)。
|
||||
|
||||
在所有相应颜色值确定以后,最终它会传到另一个阶段,我们叫做**alpha测试**和**混合**(Blending)阶段。这个阶段检测像素的相应的深度(和Stencil)值(后面会讲),使用这些,来检查这个像素是否在另一个物体的前面或后面,如此做到相应取舍。这个阶段也会查看**alpha值**(alpha值是一个物体的透明度值)和物体之间的**混合**(Blend)。所以即使在片段着色器中计算出来了一个像素所输出的颜色,最后的像素颜色在渲染多个三角形的时候也可能完全不同。
|
||||
|
||||
正如你所见的那样,图形输送管道非常复杂,它包含很多要配置的部分。然而,对于大多数场合,我们必须做的只是顶点和片段着色器。几何着色器和细分着色器是可选的,通常使用默认的着色器就行了。
|
||||
|
||||
在现代OpenGL中,我们**必须**定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。出于这个原因,开始学习现代OpenGL的时候非常困难,因为在你能够渲染自己的第一个三角形之前需要一大堆知识。这章结束就是你可以最终渲染出你的三角形的时候,你也会了解到很多图形编程知识。
|
||||
|
||||
## 顶点输入
|
||||
|
||||
开始绘制一些东西之前,我们必须给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,所以我们在OpenGL中指定的所有坐标都是在3D坐标里(x、y和z)。OpenGL不是简单的把你所有的3D坐标变换为你屏幕上的2D像素;OpenGL只是在当它们的3个轴(x、y和z)在特定的-1.0到1.0的范围内时才处理3D坐标。所有在这个范围内的坐标叫做**标准化设备坐标**(Normalized Device Coordinates)会最终显示在你的屏幕上(所有出了这个范围的都不会显示)。
|
||||
|
||||
由于我们希望渲染一个三角形,我们指定所有的这三个顶点都有一个3D位置。我们把它们以`GLfloat`数组的方式定义为标准化设备坐标(也就是在OpenGL的可见区域)中。
|
||||
|
||||
```c++
|
||||
GLfloat vertices[] = {
|
||||
-0.5f, -0.5f, 0.0f,
|
||||
0.5f, -0.5f, 0.0f,
|
||||
0.0f, 0.5f, 0.0f
|
||||
};
|
||||
```
|
||||
|
||||
由于OpenGL是在3D空间中工作的,我们渲染一个2D三角形,它的每个顶点都要有一个z坐标0.0。在这样的方式中,三角形的每一处的深度(depth)都一样,从而使它看上去就像2D的③。
|
||||
|
||||
③译注:通常深度可以理解为z坐标,它代表一个像素在空间中和你的距离,如果离你远就可能被别的像素遮挡,你就看不到它了,它会被丢弃,以节省资源。
|
||||
|
||||
<div style="border:solid #AFDFAF;border-radius:10px;background-color:#D8F5D8;margin:10px 10px 10px 0px;padding:10px">
|
||||
**标准化设备坐标**
|
||||
|
||||
一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是**标准化设备坐标**了,标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略z轴):
|
||||
|
||||

|
||||
|
||||
与通常的屏幕坐标不同,y轴正方向上的点和(0, 0)坐标是这个图像的中心,而不是左上角。最后你希望所有(变换过的)坐标都在这个坐标空间中,否则它们就不可见了。
|
||||
|
||||
你的标准化设备坐标接着会变换为**屏幕空间坐标**(screen-space coordinates),这是使用你通过`glViewport`函数提供的数据,进行**视口变换**(viewport transform)完成的。最后的屏幕空间坐标被变换为像素输入到片段着色器。
|
||||
</div>
|
||||
|
||||
有了这样的顶点数据,我们会把它作为输入发送给图形输送管道的第一个处理阶段:顶点着色器。它会在GPU上创建储存空间用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定如何发送给显卡。顶点着色器接着会处理我们告诉它要处理内存中的顶点的数量。
|
||||
|
||||
我们通过**顶点缓冲对象**(Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存储存大批顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以无论何处我们都要尝试尽量一次性发送尽可能多的数据。当数据到了显卡内存中时,顶点着色器几乎立即就能获得顶点,这非常快。
|
||||
|
||||
顶点缓冲对象(VBO)是我们在[OpenGL教程](http://www.learnopengl.com/#!Getting-Started/OpenGL)中第一个出现的OpenGL对象。就像OpenGL中的其他对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用`glGenBuffers`函数生成一个缓冲ID:
|
||||
|
||||
```c++
|
||||
GLuint VBO;
|
||||
glGenBuffers(1, &VBO);
|
||||
```
|
||||
|
||||
OpenGL有很多缓冲对象类型,`GL_ARRAY_BUFFER`是其中一个顶点缓冲对象的缓冲类型。OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用`glBindBuffer`函数把新创建的缓冲绑定到`GL_ARRAY_BUFFER`上:
|
||||
|
||||
```c++
|
||||
glBindBuffer(GL_ARRAY_BUFFER, VBO);
|
||||
```
|
||||
|
||||
从这一刻起,我们使用的任何缓冲函数(在`GL_ARRAY_BUFFER`目标上)都会用来配置当前绑定的缓冲(`VBO`)。然后我们可以调用`glBufferData`函数,它会把之前定义的顶点数据复制到缓冲的内存中:
|
||||
|
||||
```c++
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||
```
|
||||
|
||||
`glBufferData`是一个用来把用户定义数据复制到当前绑定缓冲的函数。它的第一个参数是我们希望把数据复制到上面的缓冲类型:顶点缓冲对象当前绑定到`GL_ARRAY_BUFFER`目标上。第二个参数指定我们希望传递给缓冲的数据的大小(字节);用一个简单的`sizeof`计算出顶点数据就行。第三个参数是我们希望发送的真实数据。
|
||||
|
||||
第四个参数指定了我们希望显卡如何管理给定的数据。有三种形式:
|
||||
|
||||
- GL_STATIC_DRAW:数据不会或几乎不会改变。
|
||||
- GL_DYNAMIC_DRAW:数据会被改变很多。
|
||||
- GL_STREAM_DRAW:数据每次绘制时都会改变。
|
||||
|
||||
三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它使用的类型最好是`GL_STATIC_DRAW`。如果,比如,一个缓冲中的数据将频繁被改变,那么使用的类型就是`GL_DYNAMIC_DRAW`或`GL_STREAM_DRAW`。这样就能确保图形卡把数据放在高速写入的内存部分。
|
||||
|
||||
现在我们把顶点数据储存在显卡的内存中,用VBO顶点缓冲对象管理。下面我们会创建一个顶点和片段着色器,来处理这些数据,所以我们开始创建它们吧。
|
||||
|
||||
## 顶点着色器
|
@@ -1,190 +0,0 @@
|
||||
本文作者JoeyDeVries,由[Django](http://bullteacher.com/4-hello-window.html)翻译自[http://learnopengl.com](http://www.learnopengl.com/#!Getting-started/Hello-Window)
|
||||
|
||||
# Hello Window
|
||||
|
||||
我们看看GLFW是否能够运行。首先,创建一个.cpp文件,在新创建的文件的顶部包含下面的头文件。注意,我们定义了`GLEW_STATIC`,这是因为我们将使用静态GLEW库。
|
||||
|
||||
```c++
|
||||
// GLEW
|
||||
#define GLEW_STATIC
|
||||
#include <GL/glew.h>
|
||||
// GLFW
|
||||
#include <GLFW/glfw3.h>
|
||||
```
|
||||
|
||||
<div style="border:solid #E1B3B3;border-radius:10px;background-color:#FFD2D2;margin:10px 10px 10px 0px;padding:10px">
|
||||
必须在GLFW之前引入GLEW。GLEW的头文件已经包含了OpenGL的头文件(`GL/gl.h`),所以要在其他头文件之前引入GLEW,因为它们需要有OpenGL才能起作用。
|
||||
</div>
|
||||
|
||||
下面,我们创建main函数,在main函数中我们会实例化一个GLFW窗口:
|
||||
|
||||
```c++
|
||||
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_RESIZABLE, GL_FALSE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
在main函数中我们首先使用`glfwInit`来初始化GLFW,然后我们可以使用`glfwWindowHint`来配置GLFW。`glfwWindowHint`的第一个参数告诉我们,我们打算配置哪个选项,这里我们可以从一个枚举中选择可用的选项,这些选项带有`GLFW_`前缀。第二个参数是一个整数,它代表我们为选项所设置的值。可用的选项和对应的值可以在[GLFW窗口管理文档](http://www.glfw.org/docs/latest/window.html#window_hints "GLFW窗口管理文档")中列出。现在,如果你去运行应用报出了很多未定义引用错误,这意味着你还没有成功的链接GLFW库。
|
||||
|
||||
由于这个教程关注的是OpenGL 3.3版本,我们会告诉GLFW我们使用的OpenGL版本是3.3。这样GLFW在创建**OpenGL环境**的时候可以做出合理的安排。这会保证如果一个用户没有特定的OpenGL版本,GLFW就会运行失败。我们把主和次版本都设置为3。我们同样告诉GLFW,我们希望明确地使用**核心模式(Core Profile)**,同时用户不可以调整窗口大小。显式地告诉GLFW我们希望是用核心模式会导致当我们调用一个OpenGL的遗留函数会产生**非法操作(Invalid Operation)**错误,当我们意外地使用了不该使用的旧函数时它是一个很好的提醒。注意在Mac OS X上在初始化代码里你还需要添加`glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);`才能工作。
|
||||
|
||||
<div style="border:solid #AFDFAF;border-radius:10px;background-color:#D8F5D8;margin:10px 10px 10px 0px;padding:10px">
|
||||
你需要确定你的系统/硬件上已经安装了OpenGL3.3或更高的版本,否则应用会崩溃或产生不可预测的结果。在Linux上找到你的机器上OpenGL的版本可以调用`glxinfo`,Windows机器上要使用[OpenGL Extension Viewer](http://download.cnet.com/OpenGL-Extensions-Viewer/3000-18487_4-34442.html "OpenGL Extension Viewer")这样的工具。如果支持的版本太低了,检查一下的你的显卡是否支持OpenGL3.3+(如果不支持那就太老了),先去更新你的驱动。
|
||||
</div>
|
||||
|
||||
下面我们需要创建一个窗口对象。这个窗口对象带有所有的窗口数据,它们通常是GLFW的其他函数经常使用的。
|
||||
|
||||
```c++
|
||||
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
|
||||
if (window == nullptr)
|
||||
{
|
||||
std::cout << "Failed to create GLFW window" << std::endl;
|
||||
glfwTerminate();
|
||||
return -1;
|
||||
}
|
||||
glfwMakeContextCurrent(window);
|
||||
```
|
||||
函数`glfwCreateWindow`需要窗口的**宽度**和**高度**作为它前两个参数。第三个参数允许我们给窗口创建一个名字;现在我们把它命名为`"LearnOpenGL"`但是你也可以取个自己喜欢的名字。我们**可以忽略最后两个参数**。这个函数返回一个`GLFWwindow`对象,在后面的其他GLFW操作会需要它。之后,我们告诉GLFW去创建我们窗口的环境(`glfwMakeContextCurrent`),这个环境是当前线程的主环境。
|
||||
|
||||
## GLEW
|
||||
|
||||
前面的教程里,我们提到GLEW管理着OpenGL的函数指针,所以我们希望在调用任何OpenGL函数前初始化GLEW。
|
||||
|
||||
```c++
|
||||
glewExperimental = GL_TRUE;
|
||||
if (glewInit() != GLEW_OK)
|
||||
{
|
||||
std::cout << "Failed to initialize GLEW" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
注意,在初始化GLEW前我们把`glewExperimental`变量设置为`GL_TRUE`。设置`glewExperimental`为true可以保证GLEW使用更多的现代技术来管理OpenGL功能。如果不这么设置,它就会使用默认的`GL_FALSE`,这样当使用核心模式时有可能发生问题。
|
||||
|
||||
## 视口(Viewport)
|
||||
|
||||
在我们开始渲染前,我们必须做这最后一件事。我们必须告诉OpenGL渲染窗口的大小,这样OpenGL才能知道我们希望如何设置窗口的大小和位置。我们可以通过`glViewport`函数设置这些尺寸:
|
||||
|
||||
```c++
|
||||
glViewport(0, 0, 800, 600);
|
||||
```
|
||||
|
||||
前两个参数设置了窗口**左下角的位置**。第三个和第四个参数是这个渲染窗口的**宽度**和**高度**,它和GLFW窗口是一样大的。我们可以把这个值设置得比GLFW窗口尺寸小;这样OpenGL的渲染都会在一个更小的窗口(区域)进行显示,我们可以在OpenGL的视区之外显示其他的元素。
|
||||
|
||||
<div style="border:solid #AFDFAF;border-radius:10px;background-color:#D8F5D8;margin:10px 10px 10px 0px;padding:10px">
|
||||
在幕后OpenGL是使用通过`glViewport`指定的数据将2D坐标加工为屏幕上的坐标。比如,一个被加工的点的位置是(-0.5, 0.5)会(作为它最后的变换)被映射到屏幕坐标(200, 450)上。注意,OpenGL中处理的坐标是在-1和1之间,所以我们事实上是把(-1到1)的范围映射到(0, 800)和(0, 600)上了。
|
||||
</div>
|
||||
|
||||
## 准备好你的引擎
|
||||
|
||||
我们不希望应用绘制了一个图像之后立即退出,然后关闭窗口。我们想让应用持续地绘制图像,监听用户输入直到软件被明确告知停止。为了达到这个目的,我们必须创建一个while循环,我们称其为游戏循环(Game Loop),这样,在我们告诉GLFW停止之前应用就会一直保持运行状态。下面的代码展示了一个非常简单的游戏循环。
|
||||
|
||||
```c++
|
||||
while(!glfwWindowShouldClose(window))
|
||||
{
|
||||
glfwPollEvents();
|
||||
glfwSwapBuffers(window);
|
||||
}
|
||||
```
|
||||
|
||||
`glfwWindowShouldClose`函数从开始便检验每一次循环迭代中gLFW是否已经得到关闭指示,如果得到这样的指示,函数就会返回true,并且游戏循环停止运行,之后我们就可以关闭应用了。
|
||||
|
||||
`glfwPollEvents`函数检验是否有任何事件被处触发(比如键盘输入或是鼠标移动的事件),接着调用相应函数(我们可以通过回调方法设置它们)。我们经常在循环迭代前调用事件处理函数。
|
||||
|
||||
`glfwSwapBuffers`函数会交换**颜色缓冲**(颜色缓冲是一个GLFW窗口为每一个像素储存颜色数值的大缓冲),它是在这次迭代中绘制的,也作为输出显示在屏幕上。
|
||||
|
||||
<div style="border:solid #AFDFAF;border-radius:10px;background-color:#D8F5D8;margin:10px 10px 10px 0px;padding:10px">
|
||||
**双缓冲(Double buffer)**
|
||||
|
||||
当一个应用以单缓冲方式绘制的时候,图像会产生闪缩的问题。这是因为最后的图像输出不是被立即绘制出来的,而是一个像素一个像素绘制出来的,通常是以从左到右从上到下这样的方式。由于这些图像不是立即呈现在用户面前,而是一步一步地生成结果,这就产生很多不真实感。为了规避这些问题,窗口应用使用双缓冲的方式进行渲染。**前缓冲**包含最终的输出图像,它被显示在屏幕上,与此同时,所有的渲染命令绘制**后缓冲**。所有的渲染命令执行结束,我们就把后缓冲**交换**到前缓冲,这样图像就会立即显示到用户面前了,前面提到的不真实感就这样被解决了。
|
||||
</div>
|
||||
|
||||
## 最后一件事
|
||||
|
||||
退出游戏循环后,我们就可以合理地清理/释放之前分配的所有资源了。我们可以在`main`函数结尾使用`glfwTerminate`函数来做这件事。
|
||||
|
||||
```c++
|
||||
glfwTerminate();
|
||||
return 0;
|
||||
```
|
||||
|
||||
这样会清理所有资源,并正确地退出应用。现在尝试编译你的应用,如果所有事情都工作的很好你就会看到下面的结果:
|
||||
|
||||

|
||||
|
||||
如果出现一个枯燥的黑色图像,你就做对了!如果你没有得到这个图像,或者不知道如何把所有东西组合起来,可以看我们的[完整源码](http://www.learnopengl.com/code_viewer.php?code=getting-started/hellowindow "完整源码")。
|
||||
|
||||
如果你在编译应用的时候出现状况,首先要确保链接器的所有配置都正确的做好了,在你的IDE中正确地包含了路径(前面的教程里讲过)。同时你要确保代码也是对的;你可以通过看源代码简单地去验证一下。如果仍然报错,在下面提交一个评论,写上你的问题,我或者其他人会尝试帮助你。
|
||||
|
||||
## 输入(Input)
|
||||
|
||||
我们同样希望在GLFW中有些控制输入的方式,我们可以使用GLFW的回调函数(Callback functions)做到这点。**回调函数**简单来说就是一个你可以设置的,从而使得GLFW能够在合适的时刻调用的函数指针。其中有一个我们可以设置的回调函数是**KeyCallback**函数,它在用户使用键盘交互的时候被调用。函数的原型是:
|
||||
|
||||
```c++
|
||||
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
|
||||
```
|
||||
|
||||
按键输入函数的接收一个`GLFWwindow`参数,一个代表按下按键的整型数字,一个特定动作,按钮是被按下、还是释放,一个代表某个标识的整数告诉你`shift`、`control`、`alt`或`super`是否被同时按下。每当一个用户按下一个按钮,GLFW都会调用这个函数,为你的这个函数填充合适的参数。
|
||||
|
||||
```c++
|
||||
void key_callback(GLFWwindow * window, int key, int scancode, int action, int mode)
|
||||
{
|
||||
// 当用户按下ESC, 我们就把WindowShouldClose设置为true, 关闭应用
|
||||
if(key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
|
||||
glfwSetWindowShouldClose(window, GL_TRUE);
|
||||
}
|
||||
```
|
||||
|
||||
在我们(新创建)的`key_callback`函数中,我们检查被按下的按键是否等于`ESC`健,如果是这个按键被按下(不是释放)的话,我们就用`glfwSetWindowShouldClose`设置它的`WindowShouldClose`属性为`true`来关闭GLFW。下一个主while循环条件检验会失败,应用就关闭了。
|
||||
|
||||
最后一件事就是通过GLFW使用合适的回调注册函数。可以这么做:
|
||||
|
||||
```c++
|
||||
glfwSetKeyCallback(window, key_callback);
|
||||
```
|
||||
|
||||
有许多回调函数可供用于注册我们自己的函数。比如,我们可以做一个回调函数处理窗口尺寸的改变,处理错误信息等等。我们要在创建窗口之后,在游戏循环初始化之前注册回调函数。
|
||||
|
||||
## 渲染(Rendering)
|
||||
|
||||
我们想把所有渲染命令都放在游戏循环里,因为我们打算在每个循环迭代里都执行所有的渲染命令。这看起来有点像这样:
|
||||
```c++
|
||||
// 程序循环
|
||||
while(!glfwWindowShouldClose(window))
|
||||
{
|
||||
// 检查及调用事件
|
||||
glfwPollEvents();
|
||||
|
||||
// 渲染指令放在这里
|
||||
...
|
||||
|
||||
// 交换缓冲
|
||||
glfwSwapBuffers(window);
|
||||
}
|
||||
```
|
||||
|
||||
我们用一种自己选择的颜色来清空屏幕,测试一下是否能够正常工作。每个渲染迭代的开始,我们都要清理屏幕,否则只能一直看到前一个迭代的结果(这可能就是你想要的效果,但通常你不会这么想)。我们可以使用`glClear`函数清理屏幕的颜色缓冲,在这个函数中我们以缓冲位(`BUFFER_BIT`)指定我们希望清理哪个缓冲。可用的位可以是`GL_COLOR_BUFFER_BIT`、`GL_DEPTH_BUFFER_BIT`和`GL_STENCIL_BUFFER_BIT`(译注:缓冲是显存上的一段空间,用来储存多种数据。)。现在,我们关心的只是颜色值,所以我们只清空颜色缓冲。
|
||||
|
||||
```c++
|
||||
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
```
|
||||
|
||||
要注意,我们使用了`glClearColor`来设置了清空屏幕用的颜色。当调用`glClear`去清空颜色缓冲时,全部颜色缓冲都将被`glClearColor`所配置的颜色填充。本例会填充为暗蓝绿(Dark Green-blueish)色。
|
||||
|
||||
<div style="border:solid #AFDFAF;border-radius:10px;background-color:#D8F5D8;margin:10px 10px 10px 0px;padding:10px">
|
||||
你可能会回想起OpenGL教程,`glClearColor`函数是一个状态设置函数,`glClear`是一个状态使用函数,在这个函数里,它从当前状态获取清空所用的颜色。
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
这个应用的完整代码可以在[这里](http://www.learnopengl.com/code_viewer.php?code=getting-started/hellowindow2 "这里")找到。
|
||||
|
||||
现在我们已经准备好把游戏循环用大量渲染函数填满了,但是这是下节要做的事。我认为我们已经在这里讲地够久的了。
|
Reference in New Issue
Block a user