1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 04:35:28 +08:00

Irregular update

This commit is contained in:
Meow J
2016-08-23 01:14:57 +08:00
parent 07cba71890
commit d97d795cfc
39 changed files with 1019 additions and 44 deletions

2
.gitignore vendored
View File

@@ -2,3 +2,5 @@
*.DS_Store
/site
/build
*.data
**/.Archive

View File

@@ -152,7 +152,7 @@ void main()
}
```
我们在片段着色器中声明了一个uniform `vec4`的<var>ourColor</var>并把片段着色器的输出颜色设置为uniform值的内容。因为uniform是全局变量我们我们可以在任何着色器中定义它们而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform所以我们不用在那里定义它。
我们在片段着色器中声明了一个uniform `vec4`的<var>ourColor</var>并把片段着色器的输出颜色设置为uniform值的内容。因为uniform是全局变量我们可以在任何着色器中定义它们而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform所以我们不用在那里定义它。
!!! Attention
@@ -335,11 +335,7 @@ public:
## 从文件读取
我们使用C++文件流读取着色器内容,储存到几个`string`对象里译注1
!!! note "译注1"
实际上把着色器代码保存在文件中适合学习OpenGL的时候实际开发中最好把一个着色器直接储存为多个字符串这样具有更高的灵活度。
我们使用C++文件流读取着色器内容,储存到几个`string`对象里:
```c++
Shader(const GLchar* vertexPath, const GLchar* fragmentPath)

View File

@@ -371,7 +371,7 @@ glBindVertexArray(0);
为了更熟练地使用纹理,建议在继续之后的学习之前做完这些练习:
- 修改片段着色器,**仅**笑脸图案进行翻转[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise1)
- 修改片段着色器,**仅**笑脸图案朝另一个方向看[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise1)
- 尝试用不同的纹理环绕方式,设定一个从`0.0f`到`2.0f`范围内的(而不是原来的`0.0f`到`1.0f`纹理坐标。试试看能不能在箱子的角落放置4个笑脸[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise2)[结果](http://learnopengl.com/img/getting-started/textures_exercise2.png)。记得一定要试试其他的环绕方式。
- 尝试在矩形上只显示纹理图像的中间一部分,修改纹理坐标,达到能看见单个的像素的效果。尝试使用<var>GL_NEAREST</var>的纹理过滤方式让像素显示得更清晰:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise3)
- 使用一个uniform变量作为<fun>mix</fun>函数的第三个参数来改变两个纹理可见度,使用上和下键来改变箱子或笑脸的可见度:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise4)[片段着色器](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise4_fragment)。

View File

@@ -280,13 +280,13 @@ $$
<def>位移</def>(Translation)是在原始向量的基础上加上另一个向量从而获得一个在不同位置的新向量的过程,从而在位移向量基础上**移动**了原始向量。我们已经讨论了向量加法,所以这应该不会太陌生。
和缩放矩阵一样在4×4矩阵上有几个特别的位置用来执行特定的操作对于位移来说它们是第四列最上面的3个值。如果我们把缩放向量表示为\((\color{red}{T_x},\color{green}{T_y},\color{blue}{T_z})\)我们就能把位移矩阵定义为:
和缩放矩阵一样在4×4矩阵上有几个特别的位置用来执行特定的操作对于位移来说它们是第四列最上面的3个值。如果我们把位移向量表示为\((\color{red}{T_x},\color{green}{T_y},\color{blue}{T_z})\)我们就能把位移矩阵定义为:
$$
\begin{bmatrix} \color{red}1 & \color{red}0 & \color{red}0 & \color{red}{T_x} \\ \color{green}0 & \color{green}1 & \color{green}0 & \color{green}{T_y} \\ \color{blue}0 & \color{blue}0 & \color{blue}1 & \color{blue}{T_z} \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} x + \color{red}{T_x} \\ y + \color{green}{T_y} \\ z + \color{blue}{T_z} \\ 1 \end{pmatrix}
$$
这样是能工作的,因为所有的位移值都要乘以向量的**w**所以位移值会加到向量的原始值上想想矩阵乘法法则。而如果你用3x3矩阵我们的位移值就没地方放也没地方乘了所以是不行的。
这样是能工作的,因为所有的位移值都要乘以向量的**w**所以位移值会加到向量的原始值上想想矩阵乘法法则。而如果你用3x3矩阵我们的位移值就没地方放也没地方乘了所以是不行的。
!!! Important

View File

@@ -55,7 +55,7 @@ Required library DirectX not found! Install the library (including dev packages)
这个问题的解决方案如果你之前没有安装过DirectX SDK那么请安装。下载地址[DirectX SDK](http://www.microsoft.com/en-us/download/details.aspx?id=6812)
- 安装DirectX SDK时可以遇到一个错误码为<b>S1023</b>的错误。遇到这个问题请在安装DirectX SDK前先安装C++ Redistributable package(s)。
问题解释:[已知问题DirectX SDK (June 2010) 安装及S1023错误](Known Issue: DirectX SDK (June 2010) Setup and the S1023 error)
问题解释:[已知问题DirectX SDK (June 2010) 安装及S1023错误](https://blogs.msdn.microsoft.com/chuckw/2011/12/09/known-issue-directx-sdk-june-2010-setup-and-the-s1023-error/)
- 一旦配置完成你就可以生成解决方案文件了打开解决方案文件并编译Assimp库编译为Debug版本还是Release版本根据你的需要和心情来定吧
- 使用默认配置构建的Assimp是一个动态库所以我们需要把编译出来的assimp.dll文件拷贝到我们自己程序的可执行文件的同一目录里
- 编译出来的Assimp的LIB文件和DLL文件可以在code/Debug或者code/Release里找到

View File

@@ -1,4 +1,4 @@
# 模板测试(Stencil testing)
# 模板测试
原文 | [Stencil testing](http://learnopengl.com/#!Advanced-OpenGL/Stencil-testing)
---|---

View File

@@ -1,4 +1,4 @@
# 帧缓冲Framebuffer
# 帧缓冲
原文 | [Framebuffers](http://learnopengl.com/#!Advanced-OpenGL/Framebuffers)
---|---

View File

@@ -1,4 +1,4 @@
## 抗锯齿(Anti Aliasing)
# 抗锯齿
原文 | [Anti Aliasing](http://learnopengl.com/#!Advanced-OpenGL/Anti-Aliasing)
---|---
@@ -116,9 +116,9 @@ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTI
当前绑定的帧缓冲现在有了一个纹理图像形式的多采样颜色缓冲。
### 多采样渲染缓冲对象Multisampled renderbuffer objects
### 多采样渲染缓冲对象
和纹理一样,创建一个多采样渲染缓冲对象不难。而且还很简单,因为我们所要做的全部就是当我们指定渲染缓冲的内存的时候将`glRenderbuffeStorage`改为`glRenderbufferStorageMuiltisample`
和纹理一样,创建一个多采样渲染缓冲对象(Multisampled Renderbuffer Objects)不难。而且还很简单,因为我们所要做的全部就是当我们指定渲染缓冲的内存的时候将`glRenderbuffeStorage`改为`glRenderbufferStorageMuiltisample`
```c++
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height);

View File

@@ -1,4 +1,4 @@
# 延迟着色法(Deferred Shading)
# 延迟着色法
原文 | [Deferred Shading](http://learnopengl.com/#!Advanced-Lighting/Deferred-Shading)
---|---

View File

@@ -0,0 +1,407 @@
# 调试
原文 | [Debugging](http://learnopengl.com/#!In-Practice/Debugging)
----- | ----
作者 | JoeydeVries
翻译 | [Meow J](https://github.com/Meow-J)
校对 | 暂无
图形编程可以带来很多的乐趣,然而如果什么东西渲染错误,或者甚至根本就没有渲染,它同样可以给你带来大量的沮丧感!由于我们大部分时间都在与像素打交道,当出现错误的时候寻找错误的源头可能会非常困难。调试(Debug)这样的**视觉**错误与往常熟悉的CPU调试不同。我们没有一个可以用来输出文本的控制台在GLSL代码中也不能设置断点更没有方法检测GPU的运行状态。
在这篇教程中我们将来见识几个调试OpenGL程序的技巧。OpenGL中的调试并不是很难掌握住这些技巧对之后的学习会有非常大的帮助。
## glGetError()
当你不正确使用OpenGL的时候比如说在绑定之前配置一个缓冲它会检测到并在幕后生成一个或多个用户错误标记。我们可以使用一个叫做<fun>glGetError</fun>的函数查询这些错误标记。他会检测错误标记集并且在OpenGL确实出错的时候返回一个错误值。
```c++
GLenum glGetError();
```
当<fun>glGetError</fun>被调用时,它要么会返回错误标记之一,要么返回无错误。<fun>glGetError</fun>会返回的错误值如下:
| 标记 | 代码 | 描述 |
| :------: | :------: | :------: |
| GL_NO_ERROR | 0 | 自上次调用<fun>glGetError</fun>以来没有错误 |
| GL_INVALID_ENUM | 1280 | 枚举参数不合法 |
| GL_INVALID_VALUE | 1281 | 值参数不合法 |
| GL_INVALID_OPERATION | 1282 | 一个指令的状态对指令的参数不合法 |
| GL_STACK_OVERFLOW | 1283 | 压栈操作造成栈上溢(Overflow) |
| GL_STACK_UNDERFLOW | 1284 | 弹栈操作时栈在最低点(译注:即栈下溢(Underflow) |
| GL_OUT_OF_MEMORY | 1285 | 内存调用操作无法调用(足够的)内存 |
| GL_INVALID_FRAMEBUFFER_OPERATION | 1286 | 读取或写入一个不完整的帧缓冲 |
在OpenGL的函数文档中你可以找到函数在错误时生成的错误代码。比如说如果你看一下[glBindTexture](http://docs.gl/gl3/glBindTexture)函数的文档,在 *Errors* 部分中你可以看到它可能生成的所有用户错误代码。
当一个错误标记被返回的时候,将不会报告其它的错误标记。换句话说,当<fun>glGetError</fun>被调用的时候,它会清除所有的错误标记(在分布式系统上只会清除一个,见下面的注释)。这也就是说如果你在每一帧之后调用<fun>glGetError</fun>一次,它返回一个错误,但你不能确定这就是唯一的错误,并且错误的来源可能在这一帧的任意地方。
!!! important
注意如果OpenGL是分布式(Distributely)运行的时候经常像是在X11系统上其它的用户错误代码仍然可以被生成只要它们有着不同的错误代码。调用<fun>glGetError</fun>只会重置一个错误代码标记,而不是所有。由于这一点,我们通常会建议在一个循环中调用<fun>glGetError</fun>。
```c++
glBindTexture(GL_TEXTURE_2D, tex);
std::cout << glGetError() << std::endl; // 返回 0 (无错误)
glTexImage2D(GL_TEXTURE_3D, 0, GL_RGB, 512, 512, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
std::cout << glGetError() << std::endl; // 返回 1280 (非法枚举)
glGenTextures(-5, textures);
std::cout << glGetError() << std::endl; // 返回 1281 (非法值)
std::cout << glGetError() << std::endl; // 返回 0 (无错误)
```
<fun>glGetError</fun>非常棒的一点就是它能够非常简单地定位错误可能的来源并且验证OpenGL使用的正确性。比如说你获得了一个黑屏的结果但是不知道什么造成了它是不是帧缓冲设置错误是不是我忘记绑定纹理了通过在代码中各处调用<fun>glGetError</fun>你可以非常快速地查明OpenGL错误开始出现的位置这也就意味着这次调用之前的代码中哪里出错了。
默认情况下<fun>glGetError</fun>只会打印错误数字,如果你不去记忆的话会非常难以理解。通常我们会写一个助手函数来简便地打印出错误字符串以及错误检测函数调用的位置。
```c++
GLenum glCheckError_(const char *file, int line)
{
GLenum errorCode;
while ((errorCode = glGetError()) != GL_NO_ERROR)
{
std::string error;
switch (errorCode)
{
case GL_INVALID_ENUM: error = "INVALID_ENUM"; break;
case GL_INVALID_VALUE: error = "INVALID_VALUE"; break;
case GL_INVALID_OPERATION: error = "INVALID_OPERATION"; break;
case GL_STACK_OVERFLOW: error = "STACK_OVERFLOW"; break;
case GL_STACK_UNDERFLOW: error = "STACK_UNDERFLOW"; break;
case GL_OUT_OF_MEMORY: error = "OUT_OF_MEMORY"; break;
case GL_INVALID_FRAMEBUFFER_OPERATION: error = "INVALID_FRAMEBUFFER_OPERATION"; break;
}
std::cout << error << " | " << file << " (" << line << ")" << std::endl;
}
return errorCode;
}
#define glCheckError() glCheckError_(__FILE__, __LINE__)
```
防止你不知道`__FILE__`和`__LINE__`这两个预处理指令(Preprocessor Directive)是什么,它们会在编译的时候被替换成编译时对应的文件与行号。如果我们坚持在代码中使用大量<fun>glGetError</fun>的调用,这就会让我们更加准确地知道哪个<fun>glGetError</fun>调用返回了错误(译注:记得<fun>glGetError</fun>显示的错误会发生在该次调用与上次调用之间,如果间隔太大的话需要检查的地方就太多了)。
```c++
glBindBuffer(GL_VERTEX_ARRAY, vbo);
glCheckError();
```
这会给我们以下的输出:
![](../img/06/01/debugging_glgeterror.png)
还有一个**重要的**事情需要知道GLEW有一个历史悠久的bug调用<fun>glewInit()</fun>会设置一个<var>GL_INVALID_ENUM</var>的错误标记,所以第一次调用的<fun>glGetError</fun>永远会猝不及防地给你返回一个错误代码。如果要修复这个bug我们建议您在调用<fun>glewInit</fun>之后立即调用<fun>glGetError</fun>消除这个标记:
```c++
glewInit();
glGetError();
```
<fun>glGetError</fun>并不能帮助你很多,因为它返回的信息非常简单,但不可否认它经常能帮助你检查笔误或者快速定位错误来源。总而言之,是一个非常简单但有效的工具。
## 调试输出
虽然没有<fun>glGetError</fun>普遍,但一个叫做<def>调试输出</def>(Debug Output)的OpenGL拓展十一个非常游泳的工具它在4.3版本之后变为了核心OpenGL的一部分。通过使用调试输出拓展OpenGL自身会直接发送一个比起<fun>glGetError</fun>更为完善的错误或警告信息给用户。它不仅提供了更多的信息,也能够帮助你使用一个调试器(Debugger)捕捉错误源头。
!!! important
调试输出自4.3版本变为核心OpenGL的一部分这也就是说你可以在任意运行OpenGL 4.3及以上版本的机器中找到这一功能。如果OpenGL低于这一版本你可以可以查询`ARB_debug_output `或者`AMD_debug_output`拓展来获取它的功能。注意OS X好像不支持调试输出功能从网上看到的我暂时还没有测试。如果我错了请告诉我一下
要想开始使用调试输出我们先要在初始化进程中从OpenGL请求一个调试输出上下文。这个进程根据你的窗口系统会有所不同这里我们只会讨论在GLFW中配置但你可以在教程最后的附加资源处找到其它系统的相关资料。
### GLFW中的调试输出
在GLFW中请求一个调试输出非常简单我们只需要传递一个提醒到GLFW中告诉它我们需要一个调试输出上下文即可。我们需要在调用<fun>glfwCreateWindow</fun>之前完成这一请求。
```c++
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);
```
一旦GLFW初始化完成如果我们使用的OpenGL 版本为4.3或以上的话我们就有一个调试上下文了如果不是的话则祈祷系统仍然能够请求一个调试上下文吧。如果还是不行的话我们必须使用它的OpenGL拓展来请求调试输出。
!!! important
在调试上下文中使用OpenGL会明显更缓慢一点所以当你在优化或者发布程序之前请将这一GLFW调试请求给注释掉。
要检查我们是否成功地初始化了调试上下文我们可以对OpenGL进行查询
```c++
GLint flags; glGetIntegerv(GL_CONTEXT_FLAGS, &flags);
if (flags & GL_CONTEXT_FLAG_DEBUG_BIT)
{
// 初始化调试输出
}
```
调试输出工作的方式是这样的我们首先将一个错误记录函数的回调类似于GLFW输入的回调传递给OpenGL在这个回调函数中我们可以自由地处理OpenGL错误数据在这里我们将输出一些有用的错误数据到控制台中。下面是这个就是OpenGL对调试输出所期待的回调函数的原型
```c++
void APIENTRY glDebugOutput(GLenum source, GLenum type, GLuint id, GLenum severity,
GLsizei length, const GLchar *message, void *userParam);
```
注意在OpenGL的某些实现中最后一个参数为`const void*`而不是`void*`。
有了这一大堆的数据,我们可以创建一个非常有用的错误打印工具:
```c++
void APIENTRY glDebugOutput(GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar *message,
void *userParam)
{
// 忽略一些不重要的错误/警告代码
if(id == 131169 || id == 131185 || id == 131218 || id == 131204) return;
std::cout << "---------------" << std::endl;
std::cout << "Debug message (" << id << "): " << message << std::endl;
switch (source)
{
case GL_DEBUG_SOURCE_API: std::cout << "Source: API"; break;
case GL_DEBUG_SOURCE_WINDOW_SYSTEM: std::cout << "Source: Window System"; break;
case GL_DEBUG_SOURCE_SHADER_COMPILER: std::cout << "Source: Shader Compiler"; break;
case GL_DEBUG_SOURCE_THIRD_PARTY: std::cout << "Source: Third Party"; break;
case GL_DEBUG_SOURCE_APPLICATION: std::cout << "Source: Application"; break;
case GL_DEBUG_SOURCE_OTHER: std::cout << "Source: Other"; break;
} std::cout << std::endl;
switch (type)
{
case GL_DEBUG_TYPE_ERROR: std::cout << "Type: Error"; break;
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: std::cout << "Type: Deprecated Behaviour"; break;
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: std::cout << "Type: Undefined Behaviour"; break;
case GL_DEBUG_TYPE_PORTABILITY: std::cout << "Type: Portability"; break;
case GL_DEBUG_TYPE_PERFORMANCE: std::cout << "Type: Performance"; break;
case GL_DEBUG_TYPE_MARKER: std::cout << "Type: Marker"; break;
case GL_DEBUG_TYPE_PUSH_GROUP: std::cout << "Type: Push Group"; break;
case GL_DEBUG_TYPE_POP_GROUP: std::cout << "Type: Pop Group"; break;
case GL_DEBUG_TYPE_OTHER: std::cout << "Type: Other"; break;
} std::cout << std::endl;
switch (severity)
{
case GL_DEBUG_SEVERITY_HIGH: std::cout << "Severity: high"; break;
case GL_DEBUG_SEVERITY_MEDIUM: std::cout << "Severity: medium"; break;
case GL_DEBUG_SEVERITY_LOW: std::cout << "Severity: low"; break;
case GL_DEBUG_SEVERITY_NOTIFICATION: std::cout << "Severity: notification"; break;
} std::cout << std::endl;
std::cout << std::endl;
}
```
当调试输出检测到了一个OpenGL错误它会调用这个回调函数我们将可以打印出非常多的OpenGL错误信息。注意我们忽略掉了一些错误代码这些错误代码一般不能给我们任何有用的信息比如NVidia驱动中的`131185`仅告诉我们缓冲成功创建了)。
### 过滤调试输出
有了<fun>glDebugMessageControl</fun>你可以潜在地过滤出你需要的错误类型。在这里我们不打算过滤任何来源类型或者严重等级。如果我们仅希望显示OpenGL API的高严重等级错误消息你可以设置为以下这样
```c++
glDebugMessageControl(GL_DEBUG_SOURCE_API,
GL_DEBUG_TYPE_ERROR,
GL_DEBUG_SEVERITY_HIGH,
0, nullptr, GL_TRUE);
```
有了我们的配置如果你的上下文支持调试出书的话每个不正确的OpenGL指令都会打印出一大堆的有用数据。
![](../img/06/01/debugging_debug_output.png)
### 回溯调试错误源
使用调试输出另一个很棒的技巧就是你可以很容易找出错误发生的准确行号或者调用。通过在<fun>DebugOutput</fun>中特定的错误类型上(或者在函数的顶部,如果你不关心类型的话)设置一个断点,调试器将会捕捉到抛出的错误,你可以往上查找调用栈直到找到消息发出的源头。
![](../img/06/01/debugging_debug_output_breakpoint.png)
这需要一些手动操作,但如果你大致知道你在寻找什么,这会非常有用,能够帮助你快速定位错误。
### 自定义错误输出
除了仅仅是阅读信息,我们也可以使用<fun>glDebugMessageInsert</fun>将信息推送到调试输出系统:
```c++
glDebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_ERROR, 0,
GL_DEBUG_SEVERITY_MEDIUM, -1, "error message here");
```
如果你正在利用其它使用调试输出上下文的程序或OpenGL代码进行开发这会非常有用。其它的开发者能快速了解你自定义OpenGL代码中任何**报告出来的**Bug。
总而言之,调试输出(如果你能使用它)对与快速捕捉错误是非常有用的,完全值得你花一点时间来配置,它能够省下你非常多的开发时间。你可以在[这里](http://learnopengl.com/code_viewer_gh.php?code=src/6.in_practice/1.debugging/debugging.cpp)找到源码,里面<fun>glGetError</fun>和调试输出上下文都有配置;看看你是否能够修复所有的错误。
## 调试着色器输出
对于GLSL来说我们不能访问像是<fun>glGetError</fun>这样的函数,也不能通过步进的方式运行着色器代码。如果你得到一个黑屏或者完全错误的视觉效果,通常想要知道着色器代码是否有误会非常困难。是的,我们是有编译错误报告来报告语法错误,但是捕捉语义错误又是一大难题。
一个经常使用的技巧就是将着色器程序中所有相关的变量直接发送到片段着色器的输出通道,以评估它们。通过直接输出着色器变量到输出颜色通道,我们通常可以通过观察视觉结果来获取有用的信息。比如说,如果我们想要检查一个模型的法向量是否正确,我们可以把它们(可以是变换过的也可以是没有变换过的)从顶点着色器传递到片段着色器中,在片段着色器中我们会用以下这种方式输出法向量:
```c++
#version 330 core
out vec4 FragColor;
in vec3 Normal;
[...]
void main()
{
[...]
FragColor.rgb = Normal;
FragColor.a = 1.0f;
}
```
通过输出一个(非颜色)变量到这样子的输出颜色通道中,我们可以快速审查变量是否显示着正确的值。举例来说,如果最后的视觉效果完全是黑色的,则很清楚表明法向量没有正确地传递至着色器中。当它们都显示出来的时候,检查它们(大概)正确与否就会变得非常简单。
![](../img/06/01/debugging_glsl_output.png)
从视觉效果来看我们可以看见法向量应该是正确的因为纳米装的右侧大部分都是红色的这表明法线大概正确地指向正x轴并且类似的纳米装的前方大部分都为蓝色即正z轴方向。
这一方法可以很容易拓展到你想要测试的任何变量。一旦你卡住了或者怀疑你的着色器有问题,尝试显示多个变量和/或中间结果,看看哪部分算法什么的没加上或者有错误。
## OpenGL GLSL参考编译器
每一个驱动都有它自己的怪癖。比如说NVIDIA驱动会更宽容一点通常会忽略一些限制或者规范而ATI/AMD驱动则通常会严格执行OpenGL规范在我看来会更好一点。问题是在一台机器上的着色器到另一台机器上可能就由于驱动差异不能正常工作了。
通过多年的经验你会最终能够知道不同GPU供应商之间的细微差别但如果你想要保证你的着色器代码在所有的机器上都能运行你可以直接对着官方的标准使用OpenGL的GLSL[参考编译器](https://www.khronos.org/opengles/sdk/tools/Reference-Compiler/)Reference Compiler来检查。你可以从[这里](https://www.khronos.org/opengles/sdk/tools/Reference-Compiler/)下载所谓的<def>GLSL语言校验器</def>(GLSL Lang Validator)的可执行版本,或者从[这里](https://github.com/KhronosGroup/glslang)找到完整的源码。
有了这个GLSL语言校验器你可以很方便的检查你的着色器代码只需要把着色器文件作为程序的第一格参数即可。注意GLSL语言校验器是通过下列固定的后缀名来决定着色器的类型的
- **.vert**:顶点着色器(Vertex Shader)
- **.frag**:片段着色器(Fragment Shader)
- **.geom**:几何着色器(Geometry Shader)
- **.tesc**:细分控制着色器(Tessellation Control Shader)
- **.tese**:细分评估着色器(Tessellation Evaluation Shader)
- **.comp**:计算着色器(Compute Shader)
运行GLSL参考编译器非常简单
```bash
glsllangvalidator shaderFile.vert
```
注意如果没有检测到错误的话则没有输出。对一个不正确的顶点着色器使用GLSL参考编译器进行测试会输出以下结果
![](../img/06/01/debugging_glsl_reference_compiler.png)
它不会显示AMDNVidia以及Intel的GLSL编译器之间的细微差别也不能保证你的着色器完全没有Bug但它至少能够帮你对着直接的GLSL规范进行检查。
## 帧缓冲输出
你的调试工具箱中另外一个技巧就是在OpenGL程序中一块特定区域显示帧缓冲的内容。你可能会比较频繁地使用[帧缓冲](../04 Advanced OpenGL/05 Framebuffers.md),但由于帧缓冲的魔法通常在幕后进行,有时候想要知道出什么问题会非常困难。在你的程序中显示帧缓冲的内容是一个很有用的技巧,帮助你快速检查错误。
!!! important
注意,这里讨论的帧缓冲显示内容(附件)仅能在纹理附件上使用,而不能应用于渲染缓冲对象。
通过使用一个非常简单,只显示纹理的着色器,我们可以写一个助手函数快速在屏幕右上角显示任何纹理。
```c++
// 顶点着色器
#version 330 core
layout (location = 0) in vec2 position;
layout (location = 1) in vec2 texCoords;
out vec2 TexCoords;
void main()
{
gl_Position = vec4(position, 0.0f, 1.0f);
TexCoords = texCoords;
}
// 片段着色器
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D fboAttachment;
void main()
{
FragColor = texture(fboAttachment, TexCoords);
}
```
```c++
void DisplayFramebufferTexture(GLuint textureID)
{
if(!notInitialized)
{
// 在屏幕右上角使用NDC顶点坐标初始化着色器和VAO
[...]
}
glActiveTexture(GL_TEXTURE0);
glUseProgram(shaderDisplayFBOOutput);
glBindTexture(GL_TEXTURE_2D, textureID);
glBindVertexArray(vaoDebugTexturedRect);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
glUseProgram(0);
}
int main()
{
[...]
while (!glfwWindowShouldClose(window))
{
[...]
DisplayFramebufferTexture(fboAttachment0);
glfwSwapBuffers(window);
}
}
```
这将在屏幕右上角给你一个小窗口,用来调试帧缓冲的输出。比如你想要检查延迟渲染器的几何渲染阶段中的法向量是否正确,使用这个会非常方便:
![](../img/06/01/debugging_fbo_output.png)
你当然可以拓展这个函数以支持渲染更多的纹理。这个方法能够非常快速地让你对帧缓冲内容有着持续的反馈。
## 外部调试软件
当上面所有介绍到的技巧都不能使用的时候我们仍可以使用一个第三方的工具来帮助我们调试。第三方应用通常将它们自己注入到OpenGL驱动中并且能够拦截各种OpenGL调用给你大量有用的数据。这些工具可以在很多方面帮助到你对OpenGL函数使用进行性能测试寻找瓶颈检查缓冲内存显示纹理和帧缓冲附件。如果你正在写大规模生产代码这类的工具在开发过程中是非常有用的。
我在下面列出了一些流行的调试工具,选几个尝试一下,看看哪个最适合你。
### gDebugger
gDebugger是一个非常易用的跨平台OpenGL程序调试工具。gDebugger会在你运行的OpenGL程序边上提供OpenGL状态的详细概况。你可以随时暂停程序来检查当前状态纹理内容以及缓冲使用。你可以在[这里](http://www.gremedy.com/)下载gDebugger。
运行gDebugger只需要打开程序创建一个工程给它你OpenGL程序的位置于工作目录即可。
![](../img/06/01/debugging_external_gdebugger.png)
### RenderDoc
RenderDoc是另外一个很棒的完全[开源](https://github.com/baldurk/renderdoc)的独立调试工具。和gDebugger类似你只需要设置捕捉的程序以及工作目录就行了。你的程序会正常运行当你想要检查一个特定的帧的时候你只需要让RenderDoc在程序当前状态下捕捉一个或多个帧即可。在捕捉的帧当中你可以观察管线状态所有OpenGL指令缓冲储存以及使用的纹理。
![](../img/06/01/debugging_external_renderdoc.png)
### CodeXL
[CodeXL](http://developer.amd.com/tools-and-sdks/opencl-zone/codexl/)是由AMD开发的一款GPU调试工具它有独立版本也有Visual Studio插件版本。CodeXL可以给你非常多的信息对于图形程序的性能测试也非常有用。CodeXL在NVidia与Intel的显卡上也能运行不过会不支持OpenCL调试。
![](../img/06/01/debugging_external_codexl.png)
我没有太多的CodeXL使用经验我个人觉得gDebugger和RenderDoc会更容易使用一点但我仍把它列在这里因为它仍是一个非常可靠的工具并且主要是由最大的GPU制造商之一AMD开发的。
### NVIDIA Nsight
NVIDIA流行的[Nsight](https://developer.nvidia.com/nvidia-nsight-visual-studio-edition) GPU调试工具并不是一个独立程序而是一个Visual Studio IDE或者Eclipse IDE的插件。Nsight插件对图形开发者来说非常容易使用因为它给出了GPU用量逐帧GPU状态大量运行时的统计数据。
当你在Visual Studio或Eclipse使用Nsight的调试或者性能测试指令启动程序的时候Nsight将会在程序自身中运行。Nsight非常棒的一点就是它在你的程序中渲染了一套GUI系统你可以使用它获取你程序各种各样有用的信息可以是运行时也可以是逐帧分析。
![](../img/06/01/debugging_external_nsight.png)
Nsight是一款非常有用的工具在我看来比上述其它工具都有更好的表现但它仍有一个非常重要的缺点它只能在NVIDIA的显卡上工作。如果你正在使用一款NVIDIA的显卡并且使用Visual StudioNsight是非常值得一试的。
我知道我可能遗漏了一些其它的调试工具比如我还能想到有Valve的[VOGL](https://github.com/ValveSoftware/vogl)和[APItrace](https://apitrace.github.io/)),但我觉得这个列表已经给你足够多的工具来实验了。我并不是之前提到的任何一个工具的专家,所以如果我在哪里讲错了请在评论区留言,我会很乐意改正。
## 附加资源
- [为什么你的代码会产生一个黑色窗口](http://retokoradi.com/2014/04/21/opengl-why-is-your-code-producing-a-black-window/)Reto Koradi列举了你的屏幕没有产生任何输出的可能原因。
- [调试输出](http://vallentinsource.com/opengl/debug-output)Vallentin Source写的一份非常详细的调试输出教程里面有在多个窗口系统中配置调试上下文的详细信息。

View File

@@ -4,48 +4,46 @@
---|---
作者 | JoeyDeVries
翻译 | HHz(qq:1158332489)
校对 | 暂无
校对 | Meow J
看完前面的教程之后我们已适当地了解了OpenGL内部作原理,并且我们已经能够用这些知识绘制一些复杂的图。然而,我们还只能做出一些小Demo并未达到用OpenGL开发实际应用的水平。这一部分主要介绍了一个用OpenGL制作的很简单的2D游戏。本部分教程将展现OpenGL在较大较复杂程序中的强大之处
看完前面的教程之后我们已经了解了非常多的OpenGL内部作原理,并且我们已经能够用这些知识绘制一些复杂的图。然而,除了之前的几个技术演示之外,我们还没有真正利用OpenGL开发一个实际应用。这篇教程为OpenGL 2D游戏制作系列教程的入门篇。这个系列教程将展示我们该如何将OpenGL应用到更大更复杂的环境中。注意这个系列教程不一定会引入新的OpenGL概念但会或多或少地向我们展示如何将所学的概念应用到更大的程序中去
!!! Attention
由于我们希望事情能够简单一点我们会以别人开发过的一个2D街机游戏为基础开发我们自己的2D游戏。这就是<def>Breakout</def>一个于1976年发布的运行在Atari 2600主机上的经典2D游戏。游戏要求玩家通过操控一个挡板反弹小球以破坏所有的砖块而不能让小球到达屏幕底端。当玩家破坏了所有砖块的时候即为完成了游戏。
注意本部分内容不一定引入新的OpenGL概念但会或多或少地向我们展示示如何将前面所学的概念应用到更大的程序中去。
下面我们可以看到Breakout原本在Atari 2600上是什么样子的
由于我们要做到尽可能地简化我们将开发一个别人开发过的2D电脑游戏。简单介绍一下本节的主要内容本节将开发一个1976年运行在Atari2600上的经典2D游戏控制台程序。游戏要求玩家通过操控一个挡板反弹一个小球撞倒所有的砖块并且要让小球不超出挡板边界玩家消灭所有砖块即为完成了游戏。
![](../../img/06/Breakout/01/breakout2600.png)
![](http://www.learnopengl.com/img/in-practice/breakout/breakout2600.png)
游戏有以下几个机制:
游戏有以下要点:
- 玩家能够控制一个小挡板,它只能在屏幕范围内左右移动。
- 球在屏幕上运动,每一次碰撞都会使球根据碰撞位置改变运动方向。对屏幕边框,砖块,挡板都需要有碰撞判定。
- 当球运动到屏幕底部边界的时候,游戏结束或者玩家丧失一点生命值。
- 球碰到砖块的时候,砖块会被破坏。
- 当所有砖块都被破坏,玩家获胜。
- 球的方向可以通过球碰撞在挡板上的位置(即到挡板中心的距离)来操控。
- 一个由玩家操纵且只能左右不超出屏幕边界移动的小挡板
- 球在屏幕上运动,每一次碰撞会使球 在其触及的地方改变运动方向(这与屏幕范围,砖块和挡板有关)
- 当球运动到了屏幕下边界,玩家会丧失一点生命值
- 球碰到砖块,砖块即被消去
- 砖块被消完,玩家即获胜
- 通过球打在挡板上的位置(距离挡板中心的距离),球的后续方向能够被玩家操控
因为小球不时可能会遇上砖块之间的缝隙所以它会继续弹开周围砖层进而离开缝隙。它后来若又进入了某个缺口依然遵循这个原则这就是为什么本节要叫这个名字Breakout的原因 ,小球必须 “Breakout”(弹出来).
*PS这段话不好翻译大概就是介绍打砖块的游戏规则
由于小球会不时通过缝隙到达砖墙的上方,它会在顶部边缘与砖块层的上边缘之间不断弹跳。小球会一直持续这种状态直到最终再次进入缝隙掉下来。这也是这个游戏名字的来源,小球必须要从缝隙中逃脱(Break out)出来。
# OpenGL Breakout
我们将完全利用opengl实现这个经典的电脑游戏几乎是2d游戏的基础这个版本的Breakout将能够运行在一些可出特效的显卡上。
我们将把这个经典的街机游戏作为我们之后要完全使用OpenGL实现这个游戏的基础这个版本的Breakout将运行在显卡上所以我们能够改进经典的Breakout游戏给它加一些很棒的特性
除了以上这些特性我们的Breakout还将拥有
- 精灵渲染
除了以上这些经典的机制我们的Breakout还将拥有
- 你见过最棒的画质
- 粒子系统
- 文字渲染
- 高级文字渲染
- 道具
- 后期特效
-重关卡(可自定义
- 后期处理特效
-(可自定义的)关卡
为了引起大家的兴趣,让大家看看完成本教程后所制作的游戏的样子:
为了引起大家的兴趣,你可以看看游戏完成之后的样子:
![](http://www.learnopengl.com/img/in-practice/breakout/cover.png)
![](../../img/06/Breakout/01/cover.png)
这些教程将结合前面大量概念,讲述了在之前所学的功能如何放在一起形成一个游戏。因此,最重要的是你完成了前面部分的教程
这些教程将结合之前教程中的大量概念,并且会展示如何让它们以一个整体工作。所以,在你开始这个系列教程的学习之前,请至少完成[「入门」](../../01 Getting started/01 OpenGL.md)章节
同样,有些东西也会提及其他教程的一些概念(比如说帧缓冲Framebuffers所以有必要时被提及的教程会列出
而且,有些教程会需求其它一些教程的概念(比如说在[「高级OpenGL」](../../04 Advanced OpenGL/05 Framebuffers.md)章节中的帧缓冲),所以当必要的时候我会列出需求的教程
如果你已准备好开发游戏,可以开始下一节了。
如果你已准备好开发这个游戏,可以开始[下一节](02 Setting up.md)的学习了。

View File

@@ -0,0 +1,106 @@
# 准备工作
原文 | [Setting up](http://learnopengl.com/#!In-Practice/2D-Game/Setting-up)
---|---
作者 | JoeyDeVries
翻译 | [ZMANT](https://github.com/Itanq)
校对 | 暂无
## 设置
在我们开始实际构造这个游戏之前,我们首先需要设置一些简单的框架来处理这个游戏,这个游戏将会用到几个第三方库,它们大多数都已经在前面的教程中介绍过了。不管在那个地方需要用到新的库的时候,我们都会作出适当的介绍。
首先,我们定义一个叫做`Game`的类来包含所有有关的渲染和游戏设置代码。有了这个类,我们就可以用这个类把游戏代码(稍稍的)有组织的嵌入到游戏窗口代码中。用这种方式,你就可以把相同的代码迁移到完全不同的窗口库(比如 SDL或SFML)而不需要做太多的工作。
> 存在有成千上万的方式来抽象并概括游戏/图形代码然后封装到类和对象中。在这个教程中,你刚好会看到有一个方法来解决这个问题。如果你感到有一个更好的方法,尽量实现你的改进。
这个`Game`类封装了一个初始化函数,一个更新函数,一个处理输入函数以及一个渲染函数:
```C++
class Game
{
public:
// Game state
GameState State;
GLboolean Keys[1024];
GLuint Width, Height;
// Constructor/Destructor
Game(GLuint width, GLuint height);
~Game();
// Initialize game state (load all shaders/textures/levels)
void Init();
// GameLoop
void ProcessInput(GLfloat dt);
void Update(GLfloat dt);
void Render();
};
```
这个类可能就是你期望中的游戏类。我们通过一个`width`和`height`(对应于你玩游戏时的设备分辨率)来初始化一个游戏实例并且使用`Init`函数来加载着色器、纹理以及初始化游戏状态。我们可以通过调用`ProcessInput`函数来使用存储在`Keys`数组里的数据来处理输入并且在`Update`函数里面更新游戏设置状态(比如玩家/球的移动)。最后,我们还可以在`Render`函数里面渲染游戏。注意,我们在渲染逻辑里面分离出了运动逻辑。
这个`Game`类同样了封装了一个叫做`State`的变量,它的类型`GameState`如下定义:
```C++
// Represents the current state of the game
enum GameState {
GAME_ACTIVE,
GAME_MENU,
GAME_WIN
};
```
这可以使我们跟踪游戏当前的状态。这样我们就可以根据当前游戏的状态来决定渲染或者处理不同的元素(比如当我们在游戏菜单界面的时候就可能需要渲染和处理不同的元素)。
目前为止,这个`Game`类的函数还完全是空的,因为我们还没有写实际的实现代码,但这里就是`Game`类的[header](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_setting-up.h)和[code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_setting-up)文件。
## 通用
因为我们正在开发一个大型应用所以我们将不得不频繁的重用一些OpenGL的概念比如纹理和着色器等。因此为这两个元素(items)创建一个更加易用的接口就是情理之中的事了,就像在我们前面一个教程中创建的那个`shader`类一样。
定义一个着色器类,它接收两个或三个字符串(如果有几何着色器)然后生成一个编译好的着色器(或者如果失败的话就生成一条错误信息)。这个着色器类同样也包含大量实用的函数来快速的设置`uniform`变量。同样也定义一个纹理类它从给定的字节数组中生成一个2D纹理图像(基于它的内容)。并且这个纹理类同样也封装了许多实用的函数。
我们并不打算钻研这些类实现的细节,因为学到这里你应该可以很容易的理解它们是如何工作的了。出于这个原因你可以找到它们的头文件和实现的代码文件,有详细的注释,如下:
>* Shader : [header](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/shader.h),[code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/shader)
>* Texture: [header](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/texture.h),[code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/texture)
注意到当前的这个纹理类仅仅设置成单一的`2D`纹理,但你可以很容易的通过可选的纹理类型来扩展。
## 资源管理器
当着色器和纹理类的函数被他们自己使用的时候,他们确实需要一个字节数组或是几个字符串来初始化他们。我们可以很容易的在他们自己的类里面嵌入文件的加载代码,但这就稍微有点违反`单一职责原则`(也就是说)在这个类里我们应该仅仅关注纹理或着色器本身而不需要关注他们的文件加载结构。
出于这个原因,通常用一个更加有组织的方法(来实现文件的加载),就是创建一个叫做`resource manager`的单独实体类来加载游戏相关的资源。这里有好几个方法来实现`resouce manager`;在这个教程里我们选择使用一个单独的静态`resouce manager`(就是给它静态属性),也就是说可以在整个工程中使用它来加载资源以及使用和她相关加载功能。
使用具有静态属性的单独的类有很多好处,但它主要的坏处就是会失去`OOP`特性以及控制结构/破坏。不过,这些对于小项目来说是很容易处理的。
就像其他的类文件一样,这个资源管理器的列表如下:
>* Resource Manager: [header](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/resource_manager.h), [code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/resource_manager)
使用资源管理我们可以很容易的把着色器加载到程序里面,比如:
```C++
Shader shader = ResourceManager::LoadShader("vertex.vs", "fragment.vs", nullptr, "test");
// then use it
shader.Use();
// or
ResourceManager::GetShader("test").Use();
```
通过定义的`Game`类和`resouce manager`类一起就可以很很容易管理`Shader`和`Texture2D`,基于这个基础,在以后的教程里我们将会广泛的使用这些类来实现这个`Breakout`游戏。
## 程序
对这个游戏我们仍然需要创建一个窗口并且设置OpenGL的初识状态。我们确保使用OpenGL的[面剔除](http://learnopengl.com/#!Advanced-OpenGL/Face-culling)功能和它的[混合](http://learnopengl.com/#!Advanced-OpenGL/Blending)功能。我们不需要使用深度测试;因为这个游戏完全是一个二维的,所有顶点的`z`坐标都具有相同的值。因此开启深度测试并没有什么用还有可能造成`z-fighting`现象。
这个`Breakout`游戏开始时候的代码相当的简单:我们用`GLFW`创建一个窗口,注册了一些回调函数,创建一个`Game`实例并且调用了`Game`类所有相关的函数。这个代码如下:
>* Program: [code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/program)
运行这个代码,你可能得到下面的输出:
![ans](http://learnopengl.com/img/in-practice/breakout/setting-up.png)
目前为止我们已经有了一个后面的教程需要的固定框架;我们将会持续的扩展这个`Game`类来封装一些新的功能。如果你准备好了就点击[下一个](http://learnopengl.com/#!In-Practice/2D-Game/Rendering-Sprites)教程。

View File

@@ -0,0 +1,221 @@
# 渲染精灵
原文 | [Rendering Sprites](http://learnopengl.com/#!In-Practice/2D-Game/Rendering-Sprites)
---|---
作者 | JoeyDeVires
翻译 | [ZMANT](https://github.com/Itanq)
校队 | 暂无
## 渲染精灵
为了给我们当前这个黑漆漆的游戏世界带来一点生机,我们将会渲染一些精灵(Sprite)来填补一些空虚。精灵有很多种定义但主要是指一个2D图片它通常是和一些属性数据一起使用比如用一些数据来表示它在世界坐标下的位置一个旋转的角度以及一个表示二维空间的大小的变量。在2D游戏中我们主要使用精灵来渲染图片/纹理对象。
就像前面那些教程里做的那样我们可以把顶点数据传到GPU并且手动的通过一些操作来创建一些2D形状。然而在一个大型应用中就像我们正在做的这个我们还是宁愿在渲染2D形状上做一些抽象。如果我们手动的去定义并转换每一个对象那就相当的凌乱了。
在这里我们将会定义一个用最少的代码去渲染大量的精灵的渲染类。这样我们就可以从像散沙一样的OpenGL渲染代码中摘要出游戏代码这是在一个大型应用中常用的做法。尽管我们首先要做的是设置一个合适的投影矩阵。
## 2D投影矩阵
从这个[坐标系统](../../01 Getting started/08 Coordinate Systems.md)教程我们明白了投影矩阵的作用是把视图空间坐标转化为标准化设备坐标。通过生成合适的投影矩阵,我们就可以在不同的坐标系下计算,这可能比把所有的坐标指定为标准化设备坐标(再计算)要更容易处理。
我们并不需要在坐标系执行任何的透视应用,因为这个游戏完全是在二维平面,所以一个正交投影矩阵就可以很好的工作了。因为正交投影矩阵几乎是直接把整个坐标变换到裁切空间,我们可以定义如下的矩阵来把世界坐标指定为屏幕坐标:
```c++
glm::mat4 projection = glm::ortho(0.0f,800.0f,600.0f,0.0f,-1.0f,1.0f);
```
前面的四个参数依次指定了投影椎体左、右、下、上边界。这个投影矩阵把所有在`0`到`800`之间的`x`坐标变换到`-1`到`1`之间以及把所有在`0`到`600`之间的`y`坐标变换到`-1`到`1`之间。这里我们指定了视椎体上部的`y`坐标值为`0`,同时其下部的`y`坐标值为`600`。结果就是这个场景的左上角坐标为`(0,0)`,右下角坐标为`(800,600)`,就像屏幕坐标那样。视图空间坐标直接对应像素坐标。
![](../../img/06/Breakout/03/projection.png)
这允许我们指定每个坐标都等于它们在屏幕上最终的像素坐标这在2D游戏里相当直观。
## 渲染精灵
渲染一个实际的精灵不因太复杂化。我们创建一个纹理四边形,并且在使用了前面预先定义的正交投影矩阵(变换到标准化设备坐标)之后我们可以使用模型矩阵来变换它。
!!! important
由于`Breakout`游戏是一个静态游戏,这里不需要视图/摄像机矩阵,我们可以直接使用投影矩阵把世界空间坐标变换到裁切空间坐标
为了变换精灵我们使用了下面的顶点着色器:
```c++
#version 330 core
layout (location = 0) in vec4 vertex; // <vec2 position, vec2 texCoords>
out vec2 TexCoords;
uniform mat4 model;
uniform mat4 projection;
void main()
{
TexCoords = vertex.zw;
gl_Position = projection * model * vec4(vertex.xy, 0.0, 1.0);
}
```
注意,我们使用了一个单一的`vec4`变量来存储了位置和纹理坐标数据。因为位置和纹理坐标数据都只包含了两个浮点型数据,所以我们可以把他们组合在一起作为一个单一的顶点属性。
像素着色器相对比较简单。我们设置了一个纹理和颜色向量,她们两个都会对像素最后的颜色产生影响。同样也设置了一个`uniform`颜色向量,我们就可以很容易的在游戏代码里面改变精灵的颜色。
```c++
#version 330 core
in vec2 TexCoords;
out vec4 color;
uniform sampler2D image;
uniform vec3 spriteColor;
void main()
{
color = vec4(spriteColor, 1.0) * texture(image, TexCoords);
}
```
为了让渲染精灵的代码更加有条理,我们定义了一个`SpriteRenderer`类,只需要一个单一的函数就可以渲染精灵,它的定义如下:
```c++
class SpriteRenderer
{
public:
SpriteRenderer(Shader &shader);
~SpriteRenderer();
void DrawSprite(Texture2D &texture, glm::vec2 position,
glm::vec2 size = glm::vec2(10, 10), GLfloat rotate = 0.0f,
glm::vec3 color = glm::vec3(1.0f));
private:
Shader shader;
GLuint quadVAO;
void initRenderData();
};
```
`SpriteRenderer`类封装了一个着色器对象,一个顶点数组对象以及一个渲染和初始化函数。它的构造函数接受一个着色器对象用于后面的渲染。
### 初始化
首先,让我们探究一下负责配置`quadVAO`的`initRenderData`h函数
```c++
void SpriteRenderer::initRenderData()
{
// Configure VAO/VBO
GLuint VBO;
GLfloat vertices[] = {
// Pos // Tex
0.0f, 1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f,
1.0f, 0.0f, 1.0f, 0.0f
};
glGenVertexArrays(1, &this->quadVAO);
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindVertexArray(this->quadVAO);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
```
在这我们首先定义了一组以四边形的左上角为`(0,0)`的顶点坐标。这意味着当我们在四边形上申请一个移动或伸缩变换的时候四边形的左上角会被变换。这在2D图形或GUI这类元素的位置是相对于元素左上角的系统而言很常见。
下面我们简单的向`GPU`传递了顶点数据并且配置了顶点属性,这里的情况是只有一个单一的顶点属性。我们仅仅需要为每一个精灵渲染器定义一个单一的`VAO`,因为他们的顶点数据都是一样的。
### 渲染
渲染精灵并不是太难;我们使用精灵渲染器的着色器对象,配置一个模型矩阵并且设置相关的`uniform`变量。这里最重要的就是变换的顺序:
```c++
void SpriteRenderer::DrawSprite(Texture2D &texture, glm::vec2 position,
glm::vec2 size, GLfloat rotate, glm::vec3 color)
{
// Prepare transformations
this->shader.Use();
glm::mat4 model;
model = glm::translate(model, glm::vec3(position, 0.0f));
model = glm::translate(model, glm::vec3(0.5f * size.x, 0.5f * size.y, 0.0f));
model = glm::rotate(model, rotate, glm::vec3(0.0f, 0.0f, 1.0f));
model = glm::translate(model, glm::vec3(-0.5f * size.x, -0.5f * size.y, 0.0f));
model = glm::scale(model, glm::vec3(size, 1.0f));
this->shader.SetMatrix4("model", model);
this->shader.SetVector3f("spriteColor", color);
glActiveTexture(GL_TEXTURE0);
texture.Bind();
glBindVertexArray(this->quadVAO);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
}
```
当试图在一个场景中用旋转矩阵和伸缩矩阵放置一个对象的时候,建议首先做伸缩变换,然后旋转最后是移动变换。因为矩阵乘法是从右向左执行的,所以我们变换的矩阵顺序是相反的:移动,旋转,缩放。
旋转变换可能看起来稍微有点让人望而却步。我们从[变换](../../01 Getting started/07 Transformations/)教程里面知道旋转总是围绕原点`(0,0)`转动的。因为我们指定了四边形的左上角为`(0,0)`,所有的旋转都是围绕`(0,0)`的。主要是这个旋转的原点是四边形的左上角,这样就会造成不太友好的旋转效果。我们想要做的就是把旋转的点移到四边形的中心,这样旋转就是围绕四边形中心而不是左上角了。我们通过在旋转之前把旋转点移动到四边形中心解决了这个问题。
![](../../img/06/Breakout/03/rotation-origin.png)
因为我们首先缩放了这个四边形,我们不得不在把原点变换到精灵中心的时候输入一个`size`变量(这就是我们乘了精灵的`size`向量)。一旦应用了旋转变换之后,我们就需要做上一次变换的逆变换。
把所有变换组合起来我们就可以以我们喜欢的方式移动、缩放并且旋转每一个精灵了。在下面你可以找到完整的精灵渲染器的源代码:
>* SpriteRenderer: [header](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/sprite_renderer.h),[code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/sprite_renderer)
## 你好,精灵
使用`SpriteRenderer`类我们有了把实际图片渲染到屏幕的能力!让我们来在游戏代码里面初始化一个精灵并且加载一个我们最喜爱的[纹理](../../img/06/Breakout/03/awesomeface.png)
```c++
SpriteRenderer *Renderer;
void Game::Init()
{
// Load shaders
ResourceManager::LoadShader("shaders/sprite.vs", "shaders/sprite.frag", nullptr, "sprite");
// Configure shaders
glm::mat4 projection = glm::ortho(0.0f, static_cast<GLfloat>(this->Width),
static_cast<GLfloat>(this->Height), 0.0f, -1.0f, 1.0f);
ResourceManager::GetShader("sprite").Use().SetInteger("image", 0);
ResourceManager::GetShader("sprite").SetMatrix4("projection", projection);
// Set render-specific controls
Renderer = new SpriteRenderer(ResourceManager::GetShader("sprite"));
// Load textures
ResourceManager::LoadTexture("textures/awesomeface.png", GL_TRUE, "face");
}
```
然后在渲染函数里面我们渲染一下我们的吉祥物来看看是否一切都按正确的方式工作了:
```c++
void Game::Render()
{
Renderer->DrawSprite(ResourceManager::GetTexture("face"),
glm::vec2(200, 200), glm::vec2(300, 400), 45.0f, glm::vec3(0.0f, 1.0f, 0.0f));
}
```
这里我们把精灵放置在靠近屏幕中心的位置,它的高度稍微有点大于宽度。我们同样也把它旋转了`45.0f`度并且给了一个绿色。注意,我们设定的精灵的位置是精灵四边形左上角的位置。
如果你一切都做对了你应该可以看到下面的输出:
![](../../img/06/Breakout/03/rendering-sprites.png)
你可以在[这里](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_rendering-sprites)找到更新后的游戏源代码。
目前我们的渲染系统正确工作了,我们在下一个教程里设置了游戏的等级,它在那里将会有更好的用处。

View File

@@ -0,0 +1,236 @@
# 粒子
原文 | [Particles](http://learnopengl.com/#!In-Practice/2D-Game/Particles)
----- | ----
作者 | JoeydeVries
翻译 | [ZMANT](https://github.com/Itanq)
校对 | 暂无
一个微粒,从OpenGL的角度看就是一个总是面向摄像机方向且(通常)包含一个大部分区域是透明的纹理的小四边形.一个微粒本身主要就是一个精灵(sprite),前面我们已经早就使用过了,但是当你把成千上万个这些微粒放在一起的时候,就可以创造出令人疯狂的效果.
当处理这些微粒的时候,通常是由一个叫做粒子发射器或粒子生成器的东西完成的,从这个地方,持续不断的产生新的微粒并且旧的微粒随着时间逐渐消亡.如果这个粒子发射器产生一个带着类似烟雾纹理的微粒的时候,它的颜色亮度同时又随着与发射器距离的增加而变暗,那么就会产生出灼热的火焰的效果:
![fire](http://www.learnopengl.com/img/in-practice/breakout/particles_example.jpg)
一个单一的微粒通常有一个生命值变量,并且从它产生开始就一直在缓慢的减少.一旦它的生命值少于某个极限值(通常是0)我们就会杀掉这个粒子,这样下一个粒子产生时就可以让它来替换那个被杀掉的粒子.一个粒子发射器控制它产生的所有粒子并且根据它们的属性来改变它们的行为.一个粒子通常有下面的属性:
```c++
struct Particle {
glm::vec2 Position, Velocity;
glm::vec4 Color;
GLfloat Life;
Particle()
: Position(0.0f), Velocity(0.0f), Color(1.0f), Life(0.0f) { }
};
```
看上面那个火焰的例子,那个粒子发射器可能在靠近发射器的地方产生每一个粒子,并且有一个向上的速度,这样每个粒子都是朝着正$y$轴方向移动.那似乎有3个不同区域,只是可能相比其他的区域,给了某个区域内的粒子更快的速度.我们也可以看到,$y$轴方向越高的粒子,它们的黄色或者说亮度就越低.一旦某个粒子到达某个高度的时候,它的生命值就会耗尽然后被杀掉;绝不可能直冲云霄.
你可以想象到用这样一个系统,我们就可以创造一些有趣的效果比如火焰,青烟,烟雾,魔法效果,炮火残渣等等.在Breakout游戏里,我们将会使用下面那个小球来创建一个简单的粒子生成器来制作一些有趣的效果,结果看起来就像这样:
<video src="http://learnopengl.com/video/in-practice/breakout/particles.mp4" controls="controls"></video>
上面那个粒子生成器在这个球的位置产生无数的粒子,根据球移动的速度给了粒子相应的速度,并且根据它们的生命值来改变他们的颜色亮度.
为了渲染这些粒子,我们将会用到有不同实现的着色器:
```c++
#version 330 core
layout (location = 0) in vec4 vertex; // <vec2 position, vec2 texCoords>
out vec2 TexCoords;
out vec4 ParticleColor;
uniform mat4 projection;
uniform vec2 offset;
uniform vec4 color;
void main()
{
float scale = 10.0f;
TexCoords = vertex.zw;
ParticleColor = color;
gl_Position = projection * vec4((vertex.xy * scale) + offset, 0.0, 1.0);
}
```
以及像素着色器:
```c++
#version 330 core
in vec2 TexCoords;
in vec4 ParticleColor;
out vec4 color;
uniform sampler2D sprite;
void main()
{
color = (texture(sprite, TexCoords) * ParticleColor);
}
```
我们获取每个粒子的位置和纹理属性并且设置两个uniform变量:$offset$和$color$来改变每个粒子的输出状态.注意到,在顶点着色器里,我们把这个四边形的粒子缩小了10倍;你也可以把这个缩放变量设置成uniform类型的变量从而控制一些个别的粒子.
首先,我们需要一个粒子数组,然后用Particle结构体的默认构造函数来实例化.
```c++
GLuint nr_particles = 500;
std::vector<Particle> particles;
for (GLuint i = 0; i < nr_particles; ++i)
particles.push_back(Particle());
```
然后在每一帧里面,我们都会用一个起始变量来产生一些新的粒子并且对每个粒子(还活着的)更新它们的值.
```c++
GLuint nr_new_particles = 2;
// Add new particles
for (GLuint i = 0; i < nr_new_particles; ++i)
{
int unusedParticle = FirstUnusedParticle();
RespawnParticle(particles[unusedParticle], object, offset);
}
// Uupdate all particles
for (GLuint i = 0; i < nr_particles; ++i)
{
Particle &p = particles[i];
p.Life -= dt; // reduce life
if (p.Life > 0.0f)
{ // particle is alive, thus update
p.Position -= p.Velocity * dt;
p.Color.a -= dt * 2.5;
}
}
```c++
第一个循环看起来可能有点吓人.因为这些粒子会随着时间消亡,我们就想在每一帧里面产生`nr_new_particles`个新粒子.但是一开始我们就知道了总的粒子数量是`nr_partiles`,所以我们不能简单的往粒子数组里面添加新的粒子.否则的话我们很快就会得到一个装满成千上万个粒子的数组,考虑到这个粒子数组里面其实只有一小部分粒子是存活的,这样就太浪费效率了.
我们要做的就是找到第一个消亡的粒子然后用一个新产生的粒子来更新它.函数`FirstUnuseParticle`就是试图找到第一个消亡的粒子并且返回它的索引值给调用者.
```c++
GLuint lastUsedParticle = 0;
GLuint FirstUnusedParticle()
{
// Search from last used particle, this will usually return almost instantly
for (GLuint i = lastUsedParticle; i < nr_particles; ++i){
if (particles[i].Life <= 0.0f){
lastUsedParticle = i;
return i;
}
}
// Otherwise, do a linear search
for (GLuint i = 0; i < lastUsedParticle; ++i){
if (particles[i].Life <= 0.0f){
lastUsedParticle = i;
return i;
}
}
// Override first particle if all others are alive
lastUsedParticle = 0;
return 0;
}
```
这个函数存储了它找到的上一个消亡的粒子的索引值,由于下一个消亡的粒子索引值总是在上一个消亡的粒子索引值的右边,所以我们首先从它存储的上一个消亡的粒子索引位置开始查找,如果我们没有任何消亡的粒子,我们就简单的做一个线性查找.如果没有粒子消亡就返回索引值`0`,结果就是第一个粒子被覆盖.需要注意的是,如果是最后一种情况,就意味着你粒子的生命值太长了,在每一帧里面需要产生更少的粒子,或者你只是没有保留足够的粒子.
之后,一旦粒子数组中第一个消亡的粒子被发现的时候,我们就通过调用`RespawnParticle`函数更新它的值,函数接受一个`Particle`对象,一个`GameObject`对象和一个`offset`向量:
```c++
void RespawnParticle(Particle &particle, GameObject &object, glm::vec2 offset)
{
GLfloat random = ((rand() % 100) - 50) / 10.0f;
GLfloat rColor = 0.5 + ((rand() % 100) / 100.0f);
particle.Position = object.Position + random + offset;
particle.Color = glm::vec4(rColor, rColor, rColor, 1.0f);
particle.Life = 1.0f;
particle.Velocity = object.Velocity * 0.1f;
}
```
这个函数简单的重置这个粒子的生命值为1.0f,随机的给一个大于`0.5`的颜色值(经过颜色向量)并且(在物体周围)分配一个位置和速度基于游戏里的物体.
对于更新函数里的第二个循环遍历了所有粒子,并且对于每个粒子的生命值都减去一个时间差;这样每个粒子的生命值就精确到了秒.然后再检查这个粒子是否是还活着的,若是,则更新它的位置和颜色属性.这里我们缓慢的减少粒子颜色值的`alpha`值,以至于它看起来就是随着时间而缓慢的消亡.
最后保留下来就是实际需要渲染的粒子:
```c++
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
particleShader.Use();
for (Particle particle : particles)
{
if (particle.Life > 0.0f)
{
particleShader.SetVector2f("offset", particle.Position);
particleShader.SetVector4f("color", particle.Color);
particleTexture.Bind();
glBindVertexArray(particleVAO);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
}
}
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
```
在这,对于每个粒子,我们一一设置他们的`uniform`变量`offse`和`color`,绑定纹理,然后渲染`2D`四边形的粒子.有趣的是我们在这看到了两次调用函数`glBlendFunc`.当要渲染这些粒子的时候,我们使用`GL_ONE`替换默认的目的因子模式`GL_ONE_MINUS_SRC_ALPHA`,这样,这些粒子叠加在一起的时候就会产生一些平滑的发热效果,就像在这个教程前面那样使用混合模式来渲染出火焰的效果也是可以的,这样在有大多数粒子的中心就会产生更加灼热的效果.
因为我们(就像这个系列教程的其他部分一样)喜欢让事情变得有条理,所以我们就创建了另一个类`ParticleGenerator`来封装我们刚刚谈到的所有功能.你可以在下面的链接里找到源码:
>* [header](http://www.learnopengl.com/code_viewer.php?code=in-practice/breakout/particle_generator.h),[code](http://www.learnopengl.com/code_viewer.php?code=in-practice/breakout/particle_generator)
然后在游戏代码里,我们创建这样一个粒子发射器并且用[这个](http://www.learnopengl.com/img/in-practice/breakout/textures/particle.png)纹理初始化.
```c++
ParticleGenerator *Particles;
void Game::Init()
{
[...]
ResourceManager::LoadShader("shaders/particle.vs", "shaders/particle.frag", nullptr, "particle");
[...]
ResourceManager::LoadTexture("textures/particle.png", GL_TRUE, "particle");
[...]
Particles = new ParticleGenerator(
ResourceManager::GetShader("particle"),
ResourceManager::GetTexture("particle"),
500
);
}
```
然后我们在`Game`类的`Updata`函数里为粒子生成器添加一条更新语句:
```c++
void Game::Update(GLfloat dt)
{
[...]
// Update particles
Particles->Update(dt, *Ball, 2, glm::vec2(Ball->Radius / 2));
[...]
}
```
每个粒子都将使用球的游戏对象属性对象,每帧产生两个粒子并且他们都是偏向球得中心,最后是渲染粒子:
```c++
void Game::Render()
{
if (this->State == GAME_ACTIVE)
{
[...]
// Draw player
Player->Draw(*Renderer);
// Draw particles
Particles->Draw();
// Draw ball
Ball->Draw(*Renderer);
}
}
```
注意到,我们是在渲染球体之前且在渲染其他物体之后渲染粒子的,这样,粒子就会在所有其他物体面前,但报纸在球体之后,你可以在[这里](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_particles)找到更新的`game`类的源码.
如果你现在编译并运行你的程序,你可能会看到在球体之后有一条小尾巴.就像这个教程开始的那样,给了这个游戏更加现代化的面貌.这个系统还可以很容易的扩展到更高级效果的主体上,就用这个粒子生成器自由的去实验吧,看看你是否可以创建出你自己的特效.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,5 +1,10 @@
# 欢迎来到OpenGL的世界
!!! attention
由于网站的改版,本教程现在迁移至:[https://learnopengl-cn.github.io/](https://learnopengl-cn.github.io/)。
这个网站仍然会有无规律的更新,但是大体上是不再维护了。如果要阅读最新版本的教程,还请访问以上地址。
欢迎来到OpenGL的世界。这个工程只是我([JoeyDeVries](http://joeydevries.com/))的一次小小的尝试希望能够建立起一个完善的OpenGL教学平台。无论你学习OpenGL是为了学业找工作或仅仅是因为兴趣这个网站都将能够教会你**现代**(Core-profile) OpenGL的基础中级以及高级知识。LearnOpenGL的目标是使用易于理解的形式使用清晰的例子展现现代OpenGL的所有知识点与此同时为以后的学习提供有用的参考。
## 为什么要阅读这些教程呢?

View File

@@ -12,7 +12,7 @@ pages:
- 坐标系统: '01 Getting started/08 Coordinate Systems.md'
- 摄像机: '01 Getting started/09 Camera.md'
- 复习: '01 Getting started/10 Review.md'
- 光照(Lighting):
- 光照:
- 颜色: '02 Lighting/01 Colors.md'
- 光照基础: '02 Lighting/02 Basic Lighting.md'
- 材质: '02 Lighting/03 Materials.md'
@@ -50,9 +50,13 @@ pages:
- 延迟着色法: '05 Advanced Lighting/08 Deferred Shading.md'
- SSAO: '05 Advanced Lighting/09 SSAO.md'
- 实战:
- 调试: '06 In Practice/01 Debugging.md'
- 文字渲染: '06 In Practice/02 Text Rendering.md'
- 2D游戏:
- Breakout: '06 In Practice/2D-Game/01 Breakout.md'
- 准备工作: '06 In Practice/2D-Game/02 Setting up.md'
- 渲染精灵: '06 In Practice/2D-Game/03 Rendering Sprites.md'
- 粒子: '06 In Practice/2D-Game/06 Particles.md'
site_name: LearnOpenGL-CN

BIN
old/img/01/04/ndc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
old/img/01/07/vectors.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB