1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 12:45:29 +08:00
This commit is contained in:
Meow J
2015-08-08 23:48:58 +08:00
4 changed files with 266 additions and 244 deletions

View File

@@ -1,33 +1,37 @@
##帧缓冲Framebuffers # 帧缓冲Framebuffer
本文作者JoeyDeVries由Django翻译自http://learnopengl.com 原文 | [Framebuffers](http://learnopengl.com/#!Advanced-OpenGL/Framebuffers)
---|---
作者 | JoeyDeVries
翻译 | [Django](http://bullteacher.com/)
校对 | [Geequlim](http://geequlim.com)
到目前为止,我们使用了几种不同类型的屏幕缓冲:颜色缓冲用于写入颜色值,深度缓冲用于写入深度信息,最后模板缓冲允许我们基于一些条件丢弃指定fragment。把这几种缓冲结合起来叫做帧缓冲译注framebuffer在维基百科有framebuffer的详细介绍能够帮助你更好的理解,它被储存于内存的某处。OpenGL给了我们自己定义帧缓冲的自由我们可以选择性的定义自己的颜色缓冲、以及选择性地定义深度和模板缓冲。 到目前为止,我们使用了几种不同类型的屏幕缓冲:用于写入颜色值的颜色缓冲用于写入深度信息的深度缓冲,以及允许我们基于一些条件丢弃指定片段的模板缓冲。把这几种缓冲结合起来叫做帧缓冲(Framebuffer),它被储存于内存。OpenGL给了我们自己定义帧缓冲的自由我们可以选择性的定义自己的颜色缓冲、深度和模板缓冲。
[译注1]: http://learnopengl-cn.readthedocs.org "framebuffer在维基百科有framebuffer的详细介绍能够帮助你更好的理解"
我们目前所做的渲染操作都是是在默认的帧缓冲之上进行的。当你创建了你的窗口的时候默认帧缓冲就被创建和配置好了GLFW为我们做了这件事。通过创建我们自己的帧缓冲我们能够获得一种额外的渲染方式。 我们目前所做的渲染操作都是是在默认的帧缓冲之上进行的。当你创建了你的窗口的时候默认帧缓冲就被创建和配置好了GLFW为我们做了这件事。通过创建我们自己的帧缓冲我们能够获得一种额外的渲染方式。
不会立刻理解应用程序的帧缓的含义,但是把你的场景渲染到一个不同的帧缓冲中,可以使我们能够在场景中创建镜子这样的效果,或者做出一些炫酷的后处理特效。首先我们会讨论它们是如何工作的,然后我们将利用它们来实现一些炫酷的后处理效果。 也许不能立刻理解应用程序的帧缓的含义,通过帧缓冲可以将你的场景渲染到一个不同的帧缓冲中,可以使我们能够在场景中创建镜子这样的效果,或者做出一些炫酷的特效。首先我们会讨论它们是如何工作的,然后我们将利用帧缓冲来实现一些炫酷的效果。
## 创建一个帧缓冲
就像OpenGL中其他对象一样我们可以使用一个叫做`glGenFramebuffers`的函数来创建一个帧缓冲对象简称FBO
###创建一个帧缓冲
就像OpenGL中其他对象一样我们可以使用一个叫做glGenFramebuffers的函数来创建一个帧缓冲对象简称FBO
```c++ ```c++
GLuint fbo; GLuint fbo;
glGenFramebuffers(1, &fbo); glGenFramebuffers(1, &fbo);
``` ```
这种对象的创建和使用的方式我们已经见过不少了,因此它们的使用方式也和之前我们见过的其他对象的使用方式相似首先我们要创建一个帧缓冲对象把它绑定到当前帧缓冲做一些操作然后解绑帧缓冲。我们使用glBindFramebuffer来绑定帧缓冲 这种对象的创建和使用的方式我们已经见过不少了,因此它们的使用方式也和之前我们见过的其他对象的使用方式相似首先我们要创建一个帧缓冲对象,把它绑定到当前帧缓冲,做一些操作,然后解绑帧缓冲。我们使用`glBindFramebuffer`来绑定帧缓冲:
```c++ ```c++
glBindFramebuffer(GL_FRAMEBUFFER, fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo);
``` ```
绑定到GL_FRAMEBUFFER目标后接下来所有的读、写帧缓冲的操作都会影响到当前绑定的帧缓冲。也可以把帧缓冲分开绑定到读或写目标上分别使用GL_READ_FRAMEBUFFERGL_DRAW_FRAMEBUFFER来做这件事。帧缓冲如果绑定到了GL_READ_FRAMEBUFFER就能所有读取操作像glReadPixels这样的函数使用了绑定到GL_DRAW_FRAMEBUFFER上被作为渲染、清空和其他写操作的目标。大多数时候你不必分开用通常把两个都绑定到GL_FRAMEBUFFER上就行。 绑定到`GL_FRAMEBUFFER`目标后,接下来所有的读、写帧缓冲的操作都会影响到当前绑定的帧缓冲。也可以把帧缓冲分开绑定到读或写目标上,分别使用`GL_READ_FRAMEBUFFER`或`GL_DRAW_FRAMEBUFFER`来做这件事。如果绑定到了`GL_READ_FRAMEBUFFER`,就能执行所有读取操作,像`glReadPixels`这样的函数使用了;绑定到`GL_DRAW_FRAMEBUFFER`上,就允许进行渲染、清空和其他的写入操作。大多数时候你不必分开用,通常把两个都绑定到`GL_FRAMEBUFFER`上就行。
很遗憾,现在我们不能使用自己的帧缓冲,因为还没做完呢。建构一个完整的帧缓冲必须满足以下条件: 很遗憾,现在我们不能使用自己的帧缓冲,因为还没做完呢。建构一个完整的帧缓冲必须满足以下条件:
* 我们必须往里面加入至少一个附件(颜色、深度、模板缓冲)。 * 我们必须往里面加入至少一个附件(颜色、深度、模板缓冲)。
* 其中至少有一个是颜色附件。 * 其中至少有一个是颜色附件。
@@ -36,14 +40,14 @@ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
如果你不知道什么是样本也不用担心,我们会在后面的教程中讲到。 如果你不知道什么是样本也不用担心,我们会在后面的教程中讲到。
从上面的需求中你可以看到,我们需要为帧缓冲创建一些附件,把这附件附加到帧缓冲上。当我们做完所有上面提到的条件的时候我们就可以用glCheckFramebufferStatus带上GL_FRAMEBUFFER这个参数来检查是否真的成功做到了。然后检查当前绑定的帧缓冲返回了这些规范中的哪个值。如果返回的是GL_FRAMEBUFFER_COMPLETE就对了 从上面的需求中你可以看到,我们需要为帧缓冲创建一些附件,还需要把这附件附加到帧缓冲上。当我们做完所有上面提到的条件的时候我们就可以用 `glCheckFramebufferStatus` 带上 `GL_FRAMEBUFFER` 这个参数来检查是否真的成功做到了。然后检查当前绑定的帧缓冲,返回了这些规范中的哪个值。如果返回的是 `GL_FRAMEBUFFER_COMPLETE`就对了:
```c++ ```c++
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
// Execute victory dance // Execute victory dance
``` ```
后续所有渲染操作将渲染到当前绑定的帧缓冲的附加缓冲中由于我们的帧缓冲不是默认的帧缓冲渲染命令对窗口的视频输出不会产生任何影响。出于这个原因它被称为离屏渲染off-screen rendering就是渲染到一个另外的缓冲中。为了让所有的渲染操作对主窗口产生影响我们必须通过绑定为0来使默认帧缓冲激活 后续所有渲染操作将渲染到当前绑定的帧缓冲的附加缓冲中由于我们的帧缓冲不是默认的帧缓冲渲染命令对窗口的视频输出不会产生任何影响。出于这个原因它被称为离屏渲染off-screen rendering就是渲染到一个另外的缓冲中。为了让所有的渲染操作对主窗口产生影响我们必须通过绑定为0来使默认帧缓冲激活:
```c++ ```c++
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
@@ -57,7 +61,7 @@ glDeleteFramebuffers(1, &fbo);
现在在执行完成检测前我们需要把一个或更多的附件附加到帧缓冲上。一个附件就是一个内存地址这个内存地址里面包含一个为帧缓冲准备的缓冲它可以是个图像。当创建一个附件的时候我们有两种方式可以采用纹理或渲染缓冲renderbuffer对象。 现在在执行完成检测前我们需要把一个或更多的附件附加到帧缓冲上。一个附件就是一个内存地址这个内存地址里面包含一个为帧缓冲准备的缓冲它可以是个图像。当创建一个附件的时候我们有两种方式可以采用纹理或渲染缓冲renderbuffer对象。
###纹理附件 ## 纹理附件
当把一个纹理附加到帧缓冲上的时候,所有渲染命令会写入到纹理上,就像它是一个普通的颜色/深度或者模板缓冲一样。使用纹理的好处是,所有渲染操作的结果都会被储存为一个纹理图像,这样我们就可以简单的在着色器中使用了。 当把一个纹理附加到帧缓冲上的时候,所有渲染命令会写入到纹理上,就像它是一个普通的颜色/深度或者模板缓冲一样。使用纹理的好处是,所有渲染操作的结果都会被储存为一个纹理图像,这样我们就可以简单的在着色器中使用了。
@@ -74,41 +78,41 @@ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
``` ```
这里主要的区别是我们把维度设置为屏幕代销尽管不是必须的我们还传递NULL作为纹理的data参数。对于这个纹理我们分配内存,而不去填充它。纹理填充会在渲染到帧缓冲的时候去做。同样,要注意,我们不关心放置方式或者mipmap的事,因为在大多数时候都不会需要它们的。 这里主要的区别是我们把纹理的维度设置为屏幕大小尽管不是必须的我们还传递NULL作为纹理的data参数。对于这个纹理我们分配内存,而不去填充它。纹理填充会在渲染到帧缓冲的时候去做。同样,要注意,我们不关心环绕方式或者Mipmap因为在大多数时候都不会需要它们的。
如果你打算把整个屏幕渲染到一个或大或小的纹理上,你需要用新的纹理的尺寸作为参数再次调用`glViewport`(要在渲染到你的帧缓冲之前做好),否则只有一小部分纹理或屏幕能够绘制到纹理上。
如果你打算把整个屏幕渲染到一个或大或小的纹理上你需要用新的纹理的尺寸作为参数再次调用glViewport要在渲染到你的帧缓冲之前做好否则只有一小部分纹理或屏幕能够绘制到纹理上。
现在我们已经创建了一个纹理,最后一件要做的事情是把它附加到帧缓冲上: 现在我们已经创建了一个纹理,最后一件要做的事情是把它附加到帧缓冲上:
```c++ ```c++
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D, texture, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D, texture, 0);
``` ```
glFramebufferTexture2D函数有以下参数
`glFramebufferTexture2D`函数需要传入下列参数:
* target我们所创建的帧缓冲类型的目标绘制、读取或两者都有 * target我们所创建的帧缓冲类型的目标绘制、读取或两者都有
* attachment我们所附加的附件的类型。现在我们附加的是一个颜色附件。需要注意最后的那个0是暗示我们可以附加1个以上颜色的附件。我们会在后面的教程中谈到。 * attachment我们所附加的附件的类型。现在我们附加的是一个颜色附件。需要注意最后的那个0是暗示我们可以附加1个以上颜色的附件。我们会在后面的教程中谈到。
* textarget你希望附加的纹理类型。 * textarget你希望附加的纹理类型。
* texture附加的实际纹理。 * texture附加的实际纹理。
* levelmipmap level。我们设置为0. * levelMipmap level。我们设置为0
除颜色附件以外我们还可以附加一个深度和一个模板纹理到帧缓冲对象上。为了附加一个深度缓冲我们可以知道那个GL_DEPTH_ATTACHMENT作为附件类型。记住这时纹理格式和内部格式类型internalformat就成了GL_DEPTH_COMPONENT去反应深度缓冲的存储格式。附加一个模板缓冲你要使用GL_STENCIL_ATTACHMENT作为第二个参数把纹理格式指定为GL_STENCIL_INDEX。 除颜色附件以外,我们还可以附加一个深度和一个模板纹理到帧缓冲对象上。为了附加一个深度缓冲,我们可以知道那个`GL_DEPTH_ATTACHMENT`作为附件类型。记住这时纹理格式和内部格式类型internalformat就成了 `GL_DEPTH_COMPONENT`去反应深度缓冲的存储格式。附加一个模板缓冲,你要使用 `GL_STENCIL_ATTACHMENT`作为第二个参数,把纹理格式指定为 `GL_STENCIL_INDEX`
也可以同时附加一个深度缓冲和一个模板缓冲为一个单独的纹理。这样纹理的每32位数值就包含了24位的深度信息和8位的模板信息。为了把一个深度和模板缓冲附加到一个单独纹理上我们使用GL_DEPTH_STENCIL_ATTACHMENT类型配置纹理格式以包含深度值和模板值的结合物。下面是一个附加了深度和模板缓冲为单一纹理的例子 也可以同时附加一个深度缓冲和一个模板缓冲为一个单独的纹理。这样纹理的每32位数值就包含了24位的深度信息和8位的模板信息。为了把一个深度和模板缓冲附加到一个单独纹理上我们使用`GL_DEPTH_STENCIL_ATTACHMENT`类型配置纹理格式以包含深度值和模板值的结合物。下面是一个附加了深度和模板缓冲为单一纹理的例子:
```c++ ```c++
glTexImage2D( glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 800, 600, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL );
GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 800, 600, 0,
GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL
);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);
``` ```
渲染缓冲对象附件Renderbuffer object attachments
### 渲染缓冲对象附件Renderbuffer object attachments
在介绍了帧缓冲的可行附件类型——纹理后OpenGL引进了渲染缓冲对象Renderbuffer objects所以在过去那些美好时光里纹理是附件的唯一可用的类型。和纹理图像一样渲染缓冲对象也是一个缓冲它可以是一堆字节、整数、像素或者其他东西。渲染缓冲对象的一大优点是它以OpenGL原生渲染格式储存它的数据因此在离屏渲染到帧缓冲的时候这些数据就相当于被优化过的了。 在介绍了帧缓冲的可行附件类型——纹理后OpenGL引进了渲染缓冲对象Renderbuffer objects所以在过去那些美好时光里纹理是附件的唯一可用的类型。和纹理图像一样渲染缓冲对象也是一个缓冲它可以是一堆字节、整数、像素或者其他东西。渲染缓冲对象的一大优点是它以OpenGL原生渲染格式储存它的数据因此在离屏渲染到帧缓冲的时候这些数据就相当于被优化过的了。
渲染缓冲对象将所有渲染数据直接储存到它们的缓冲里,而不会进行针对特定纹理格式的任何转换,这样它们就成了一种快速可写的存储介质了。然而,渲染缓冲对象通常是只写的,不能读取它们就像获取纹理不能写入纹理一样。可以用glReadPixels函数去读取函数返回一个当前绑定的帧缓冲的特定像素区域而不是直接返回附件本身。 渲染缓冲对象将所有渲染数据直接储存到它们的缓冲里,而不会进行针对特定纹理格式的任何转换,这样它们就成了一种快速可写的存储介质了。然而,渲染缓冲对象通常是只写的,不能修改它们(就像获取纹理,不能写入纹理一样)。可以用`glReadPixels`函数去读取,函数返回一个当前绑定的帧缓冲的特定像素区域,而不是直接返回附件本身。
因为它们的数据已经是原生格式了,在写入或把它们的数据简单地到其他缓冲的时候非常快。当使用渲染缓冲对象时,像切换缓冲这种操作变得异常高速。我们在个渲染迭代末尾使用的那个glfwSwapBuffers函数同样以渲染缓冲对象实现我们简单地写入到一个渲染缓冲图像最后交换到另一个里。渲染缓冲对象对于这种操作来说很完美。 因为它们的数据已经是原生格式了,在写入或把它们的数据简单地到其他缓冲的时候非常快。当使用渲染缓冲对象时,像切换缓冲这种操作变得异常高速。我们在个渲染迭代末尾使用的那个`glfwSwapBuffers`函数,同样以渲染缓冲对象实现:我们简单地写入到一个渲染缓冲图像,最后交换到另一个里。渲染缓冲对象对于这种操作来说很完美。
创建一个渲染缓冲对象和创建帧缓冲代码差不多: 创建一个渲染缓冲对象和创建帧缓冲代码差不多:
@@ -117,21 +121,21 @@ GLuint rbo;
glGenRenderbuffers(1, &rbo); glGenRenderbuffers(1, &rbo);
``` ```
相似地,我们打算把渲染缓冲对象绑定,这样所有后续渲染缓冲操作都会影响到当前的RBO 相似地,我们打算把渲染缓冲对象绑定,这样所有后续渲染缓冲操作都会影响到当前的渲染缓冲对象
```c++ ```c++
glBindRenderbuffer(GL_RENDERBUFFER, rbo); glBindRenderbuffer(GL_RENDERBUFFER, rbo);
``` ```
由于渲染缓冲对象通常是只写的,它们经常作为深度和模板附件来使用,由于大多数时候,我们不需要从深度和模板缓冲中读取数据,但仍关深度和模板测试。我们就需要有深度和模板值提供给测试但不需要对这些值进行采样sample所以深度缓冲对象是完全符合的。当我们不去从这些缓冲中采样的时候渲染缓冲对象通常很合适因为它们等于是优化过的。 由于渲染缓冲对象通常是只写的,它们经常作为深度和模板附件来使用,由于大多数时候,我们不需要从深度和模板缓冲中读取数据,但仍关深度和模板测试。我们就需要有深度和模板值提供给测试但不需要对这些值进行采样sample所以深度缓冲对象是完全符合的。当我们不去从这些缓冲中采样的时候渲染缓冲对象通常很合适因为它们等于是优化过的。
调用glRenderbufferStorage函数可以创建一个深度和模板渲染缓冲对象 调用`glRenderbufferStorage`函数可以创建一个深度和模板渲染缓冲对象:
```c ```c
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
``` ```
创建一个渲染缓冲对象与创建纹理对象相似不同之处在于这个对象是专门被设计用于图像的而不是通用目的的数据缓冲比如纹理。这里我们选择GL_DEPTH24_STENCIL8作为内部格式它同时代表24位的深度和8位的模板缓冲。 创建一个渲染缓冲对象与创建纹理对象相似,不同之处在于这个对象是专门被设计用于图像的,而不是通用目的的数据缓冲,比如纹理。这里我们选择`GL_DEPTH24_STENCIL8`作为内部格式它同时代表24位的深度和8位的模板缓冲。
最后一件还要做的事情是把帧缓冲对象附加上: 最后一件还要做的事情是把帧缓冲对象附加上:
@@ -141,7 +145,7 @@ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDER
在帧缓冲项目中,渲染缓冲对象可以提供一些优化,但更重要的是知道何时使用渲染缓冲对象,何时使用纹理。通常的规则是,如果你永远都不需要从特定的缓冲中进行采样,渲染缓冲对象对特定缓冲是更明智的选择。如果哪天需要从比如颜色或深度值这样的特定缓冲采样数据的话,你最好还是使用纹理附件。从执行效率角度考虑,它不会对效率有太大影响。 在帧缓冲项目中,渲染缓冲对象可以提供一些优化,但更重要的是知道何时使用渲染缓冲对象,何时使用纹理。通常的规则是,如果你永远都不需要从特定的缓冲中进行采样,渲染缓冲对象对特定缓冲是更明智的选择。如果哪天需要从比如颜色或深度值这样的特定缓冲采样数据的话,你最好还是使用纹理附件。从执行效率角度考虑,它不会对效率有太大影响。
###渲染到纹理 ### 渲染到纹理
现在我们知道了(一些)帧缓冲如何工作的,是时候把它们用起来了。我们会把场景渲染到一个颜色纹理上,这个纹理附加到一个我们创建的帧缓冲上,然后把纹理绘制到一个简单的四边形上,这个四边形铺满整个屏幕。输出的图像看似和没用帧缓冲一样,但是这次,它其实是直接打印到了一个单独的四边形上面。为什么这很有用呢?下一部分我们会看到原因。 现在我们知道了(一些)帧缓冲如何工作的,是时候把它们用起来了。我们会把场景渲染到一个颜色纹理上,这个纹理附加到一个我们创建的帧缓冲上,然后把纹理绘制到一个简单的四边形上,这个四边形铺满整个屏幕。输出的图像看似和没用帧缓冲一样,但是这次,它其实是直接打印到了一个单独的四边形上面。为什么这很有用呢?下一部分我们会看到原因。
@@ -171,7 +175,7 @@ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texC
我们同样打算要让OpenGL确定可以进行深度测试模板测试如果你用的话所以我们必须还要确保向帧缓冲中添加一个深度和模板附件。由于我们只采样颜色缓冲并不采样其他缓冲我们可以创建一个渲染缓冲对象来达到这个目的。记住当你不打算从指定缓冲采样的的时候它们是一个不错的选择。 我们同样打算要让OpenGL确定可以进行深度测试模板测试如果你用的话所以我们必须还要确保向帧缓冲中添加一个深度和模板附件。由于我们只采样颜色缓冲并不采样其他缓冲我们可以创建一个渲染缓冲对象来达到这个目的。记住当你不打算从指定缓冲采样的的时候它们是一个不错的选择。
创建一个渲染缓冲对象不太难。唯一一件要记住的事情是我们正在创建的是一个渲染缓冲对象的深度和模板附件。我们把它的内部给事设置为GL_DEPTH24_STENCIL8对于我们的目的来说这个精确度已经足够了。 创建一个渲染缓冲对象不太难。唯一一件要记住的事情是,我们正在创建的是一个渲染缓冲对象的深度和模板附件。我们把它的内部给事设置为`GL_DEPTH24_STENCIL8`,对于我们的目的来说这个精确度已经足够了。
```c++ ```c++
GLuint rbo; GLuint rbo;
@@ -185,7 +189,7 @@ glBindRenderbuffer(GL_RENDERBUFFER, 0);
接着,在做好帧缓冲之前,还有最后一步,我们把渲染缓冲对象附加到帧缓冲的深度和模板附件上: 接着,在做好帧缓冲之前,还有最后一步,我们把渲染缓冲对象附加到帧缓冲的深度和模板附件上:
```c ```c++
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
``` ```
@@ -199,16 +203,17 @@ glBindFramebuffer(GL_FRAMEBUFFER, 0);
还要保证解绑帧缓冲,这样我们才不会意外渲染到错误的帧缓冲上。 还要保证解绑帧缓冲,这样我们才不会意外渲染到错误的帧缓冲上。
现在帧缓冲做好了,我们要做的全部就是渲染到帧缓冲的还冲上,而不是绑定到帧缓冲对象的默认缓冲。余下所有命令会影响到当前绑定的帧缓冲上。所有深度和模板操作同样会从当前绑定的帧缓冲的深度和模板附件中读取,当然,得是在它们可用的情况下。如果你遗漏了比如深度缓冲,所有深度测试就不会工作,因为当前绑定的帧缓冲里没有深度缓冲。 现在帧缓冲做好了,我们要做的全部就是渲染到帧缓冲上,而不是绑定到帧缓冲对象的默认缓冲。余下所有命令会影响到当前绑定的帧缓冲上。所有深度和模板操作同样会从当前绑定的帧缓冲的深度和模板附件中读取,当然,得是在它们可用的情况下。如果你遗漏了比如深度缓冲,所有深度测试就不会工作,因为当前绑定的帧缓冲里没有深度缓冲。
所以,为把场景绘制到一个单独的纹理,我们必须以下面步骤来做: 所以,为把场景绘制到一个单独的纹理,我们必须以下面步骤来做:
使用新的绑定为激活帧缓冲的帧缓冲,像往常那样渲染场景。 1. 使用新的绑定为激活帧缓冲的帧缓冲,像往常那样渲染场景。
绑定到默认帧缓冲。 2. 绑定到默认帧缓冲。
绘制一个四边形,让它平铺到整个屏幕上,用新的帧缓冲的颜色缓冲作为他的纹理。 3. 绘制一个四边形,让它平铺到整个屏幕上,用新的帧缓冲的颜色缓冲作为他的纹理。
我们使用在深度测试教程中同一个场景进行绘制,但是这次使用老气横秋的箱子纹理。
为了绘制四边形我们将会创建新的着色器。我们不打算引入任何花哨的变换矩阵,因为我们只提供已经是标准化设备坐标的顶点坐标,所以我们可以直接把它们作为顶点着色器的输出。顶点着色器看起来像这样: 我们使用在深度测试教程中同一个场景进行绘制,但是这次使用老气横秋的[箱子纹理](http://learnopengl.com/img/textures/container.jpg)。
为了绘制四边形我们将会创建新的着色器。我们不打算引入任何花哨的变换矩阵,因为我们只提供已经是标准化设备坐标的[顶点坐标](http://learnopengl.com/code_viewer.php?code=advanced/framebuffers_quad_vertices),所以我们可以直接把它们作为顶点着色器的输出。顶点着色器看起来像这样:
```c++ ```c++
#version 330 core #version 330 core
@@ -262,7 +267,7 @@ glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0); glBindVertexArray(0);
``` ```
只有很少的事情要说明。第一由于我们用的每个帧缓冲都有自己的一系列缓冲我们打算使用glClear设置的合适的位bits来清空这些缓冲。第二当渲染四边形的时候我们关闭深度测试因为我们不关系深度测试我们绘制的是一个简单的四边形当我们绘制普通场景时我们必须再次开启深度测试。 只有很少的事情要说明。第一,由于我们用的每个帧缓冲都有自己的一系列缓冲,我们打算使用`glClear`设置的合适的位bits来清空这些缓冲。第二当渲染四边形的时候我们关闭深度测试因为我们不关系深度测试我们绘制的是一个简单的四边形当我们绘制普通场景时我们必须再次开启深度测试。
这里的确有很多地方会做错,所以如果你没有获得任何输出,尝试排查任何可能出现错误的地方,再次阅读教程中相关章节。如果每件事都做对了就一定能成功,你将会得到这样的输出: 这里的确有很多地方会做错,所以如果你没有获得任何输出,尝试排查任何可能出现错误的地方,再次阅读教程中相关章节。如果每件事都做对了就一定能成功,你将会得到这样的输出:
@@ -270,16 +275,16 @@ glBindVertexArray(0);
左侧展示了和深度测试教程中一样的输出结果,但是这次却是渲染到一个简单的四边形上的。如果我们以线框方式显示的话,那么显然,我们只是绘制了一个默认帧缓冲中单调的四边形。 左侧展示了和深度测试教程中一样的输出结果,但是这次却是渲染到一个简单的四边形上的。如果我们以线框方式显示的话,那么显然,我们只是绘制了一个默认帧缓冲中单调的四边形。
你可以从这里得到应用的源码。 你可以[从这里得到应用的源码](http://learnopengl.com/code_viewer.php?code=advanced/framebuffers_screen_texture)
然而这有什么好处呢?好处就是我们现在可以自由的获取已经渲染场景中的任何像素,然后把它当作一个纹理图像了,我们可以在片段着色器中创建一些有意思的效果。所有这些有意思的效果统称为后处理特效。 然而这有什么好处呢?好处就是我们现在可以自由的获取已经渲染场景中的任何像素,然后把它当作一个纹理图像了,我们可以在片段着色器中创建一些有意思的效果。所有这些有意思的效果统称为后处理特效。
###后处理 ### 后处理
现在,整个场景渲染到了一个单独的纹理上,我们可以创建一些有趣的效果,只要简单操纵纹理数据就能做到。这部分,我们会向你展示一些流行的后处理特效,以及怎样添加一些创造性去创建出你自己的特效。 现在,整个场景渲染到了一个单独的纹理上,我们可以创建一些有趣的效果,只要简单操纵纹理数据就能做到。这部分,我们会向你展示一些流行的后处理特效,以及怎样添加一些创造性去创建出你自己的特效。
###反相 ### 反相
我们已经取得了渲染输出的每个颜色所以在片段着色器里返回这些颜色的反色并不难。我们得到屏幕纹理的颜色然后用1.0减去它: 我们已经取得了渲染输出的每个颜色所以在片段着色器里返回这些颜色的反色并不难。我们得到屏幕纹理的颜色然后用1.0减去它:
@@ -296,7 +301,7 @@ void main()
整个场景现在的颜色都反转了,只需在着色器中写一行代码就能做到,酷吧? 整个场景现在的颜色都反转了,只需在着色器中写一行代码就能做到,酷吧?
###灰度 ### 灰度
另一个有意思的效果是移除所有除了黑白灰以外的颜色作用,是整个图像成为黑白的。实现它的简单的方式是获得所有颜色元素,然后将它们平均化: 另一个有意思的效果是移除所有除了黑白灰以外的颜色作用,是整个图像成为黑白的。实现它的简单的方式是获得所有颜色元素,然后将它们平均化:
@@ -319,9 +324,9 @@ void main()
} }
``` ```
![image description](http://learnopengl.com/img/advanced/framebuffers_grayscale.png) ![](http://learnopengl.com/img/advanced/framebuffers_grayscale.png)
###Kernel effects ### Kernel effects
在单独纹理图像上进行后处理的另一个好处是我们可以从纹理的其他部分进行采样。比如我们可以从当前纹理值的周围采样多个纹理值。创造性地把它们结合起来就能创造出有趣的效果了。 在单独纹理图像上进行后处理的另一个好处是我们可以从纹理的其他部分进行采样。比如我们可以从当前纹理值的周围采样多个纹理值。创造性地把它们结合起来就能创造出有趣的效果了。
@@ -381,11 +386,11 @@ void main()
这里创建的有趣的效果就好像你的玩家吞了某种麻醉剂产生的幻觉一样。 这里创建的有趣的效果就好像你的玩家吞了某种麻醉剂产生的幻觉一样。
###Blur ### Blur
创建模糊效果的kernel定义如下 创建模糊效果的kernel定义如下
![](http://learnopengl-cn.readthedocs.org/zh/latest/img/05_05framebuffers_ kernel_sample.png) ![](http://learnopengl-cn.readthedocs.org/zh/latest/img/05_05_blur_sample.png)
由于所有数值加起来的总和为16,简单返回结合起来的采样颜色是非常亮的,所以我们必须将kernel的每个值除以16.最终的kernel数组会是这样的: 由于所有数值加起来的总和为16,简单返回结合起来的采样颜色是非常亮的,所以我们必须将kernel的每个值除以16.最终的kernel数组会是这样的:
@@ -405,7 +410,7 @@ float kernel[9] = float[](
你可以看到我们一旦拥有了这个kernel的实现以后,创建一个后处理特效就不再是一件难事.最后,我们再来讨论一个流行的特效,以结束本节内容. 你可以看到我们一旦拥有了这个kernel的实现以后,创建一个后处理特效就不再是一件难事.最后,我们再来讨论一个流行的特效,以结束本节内容.
###边检测 ### 边检测
下面的边检测kernel与锐化kernel类似: 下面的边检测kernel与锐化kernel类似:
@@ -415,8 +420,9 @@ float kernel[9] = float[](
![](http://learnopengl.com/img/advanced/framebuffers_edge_detection.png) ![](http://learnopengl.com/img/advanced/framebuffers_edge_detection.png)
在一些像Photoshop这样的软件中使用这些kernel作为图像操作工具/过滤器一点都不奇怪.因为掀开可以具有很强的平行处理能力,我们以实时进行针对每个像素的图像操作便相对容易.图像编辑工具因而更经常使用显卡来进行图像处理. 在一些像Photoshop这样的软件中使用这些kernel作为图像操作工具/过滤器一点都不奇怪.因为掀开可以具有很强的平行处理能力我们以实时进行针对每个像素的图像操作便相对容易图像编辑工具因而更经常使用显卡来进行图像处理
###练习 ## 练习
你可以使用帧缓冲来创建一个后视镜吗?做到它,你必须绘制场景两次:一次正常绘制,另一次摄像机旋转180度后绘制.尝试在你的显示器顶端创建一个小四边形,在上面应用后视镜的镜面纹理:解决方案,视觉效果.
自己随意调整一下kernel值,创建出你自己后处理特效.尝试在网上搜索其他有趣的kernel. * 你可以使用帧缓冲来创建一个后视镜吗?做到它,你必须绘制场景两次:一次正常绘制,另一次摄像机旋转180度后绘制.尝试在你的显示器顶端创建一个小四边形,在上面应用后视镜的镜面纹理:[解决方案](http://learnopengl.com/code_viewer.php?code=advanced/framebuffers-exercise1),[视觉效果](http://learnopengl.com/img/advanced/framebuffers_mirror.png)
* 自己随意调整一下kernel值,创建出你自己后处理特效.尝试在网上搜索其他有趣的kernel.

View File

@@ -1,8 +1,12 @@
## Cubemaps # Cubemaps
本文作者JoeyDeVries由Django翻译自http://learnopengl.com 原文 | [Cubemaps](http://learnopengl.com/#!Advanced-OpenGL/Cubemaps)
---|---
作者 | JoeyDeVries
翻译 | [Django](http://bullteacher.com/)
校对 | [Geequlim](http://geequlim.com)
我们之前一直使用的是2D纹理但是还有更多的纹理类型我们没有探索过本教程中我们讨论的纹理类型是将多个纹理组合起来映射到一个单一纹理它就是cubemap。 我们之前一直使用的是2D纹理还有更多的纹理类型我们没有探索过本教程中我们讨论的纹理类型是将多个纹理组合起来映射到一个单一纹理它就是cubemap。
基本上说cubemap它包含6个2D纹理这每个2D纹理是一个立方体cube的一个面也就是说它是一个有贴图的立方体。你可能会奇怪这样的立方体有什么用为什么费事地把6个独立纹理结合为一个单独的纹理只使用6个各自独立的不行吗这是因为cubemap有自己特有的属性可以使用方向向量对它们索引和采样。想象一下我们有一个1×1×1的单位立方体有个以原点为起点的方向向量在它的中心。 基本上说cubemap它包含6个2D纹理这每个2D纹理是一个立方体cube的一个面也就是说它是一个有贴图的立方体。你可能会奇怪这样的立方体有什么用为什么费事地把6个独立纹理结合为一个单独的纹理只使用6个各自独立的不行吗这是因为cubemap有自己特有的属性可以使用方向向量对它们索引和采样。想象一下我们有一个1×1×1的单位立方体有个以原点为起点的方向向量在它的中心。
@@ -12,14 +16,14 @@
!!! Important !!! Important
方向向量的大小无关紧要。一旦提供了方向OpenGL就会获取方向向量触碰上的相应的纹理像素texel这样就返回了正确的纹理采样值。 方向向量的大小无关紧要。一旦提供了方向OpenGL就会获取方向向量触碰到立方体表面上的相应的纹理像素texel这样就返回了正确的纹理采样值。
方向向量触碰上cubemap的一点意味着我们可以使用cubemap的位置向量来对cubemap进行采样只要cubemap位于原点。然后我们就可以获取所有顶点的纹理坐标,就好像cubemap上有顶点位置一样。所获得的结果是一个纹理坐标,这个纹理坐标获取cubemap上正确的那个面的纹理。 方向向量触碰到立方体表面的一点也就是cubemap的纹理位置这意味着只要立方体的中心位于原点上我们可以使用立方体的位置向量来对cubemap进行采样。然后我们就可以获取所有顶点的纹理坐标和立方体上的顶点位置一样。所获得的结果是一个纹理坐标,通过这个纹理坐标就能获取cubemap上正确的纹理。
创建一个cubemap ### 创建一个Cubemap
cubemap和其他纹理一样所以要创建一个cubemap在进行任何纹理操作之前需要生成一个纹理激活相应纹理单元然后绑定到合适的纹理目标上。这次要绑定到GL_TEXTURE_CUBE_MAP Cubemap和其他纹理一样所以要创建一个cubemap在进行任何纹理操作之前需要生成一个纹理激活相应纹理单元然后绑定到合适的纹理目标上。这次要绑定到 `GL_TEXTURE_CUBE_MAP`纹理类型
```c++ ```c++
GLuint textureID; GLuint textureID;
@@ -27,11 +31,11 @@ glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
``` ```
由于cubemap包含6个纹理每个面一个我们必须调用glTexImage2D函数6次函数的参数和前面教程讲的相似。然而这次我们必须把纹理目标target参数设置为cubemap特定的面这是告诉OpenGL我们创建的纹理是对应cubemap哪个面的。此我们便需要为cubemap的每个面调用一次glTexImage2D 由于cubemap包含6个纹理立方体的每个面一个纹理,我们必须调用`glTexImage2D`函数6次函数的参数和前面教程讲的相似。然而这次我们必须把纹理目标target参数设置为cubemap特定的面这是告诉OpenGL我们创建的纹理是对应立方体哪个面的。此我们便需要为cubemap的每个面调用一次 `glTexImage2D`
由于我们有6个面OpenGL就提供了6个不同的纹理目标来应对cubemap的面。 由于cubemap有6个面OpenGL就提供了6个不同的纹理目标来应对cubemap的各个面。
纹理目标Texture target |方位 纹理目标Texture target | 方位
---|--- ---|---
GL_TEXTURE_CUBE_MAP_POSITIVE_X | 右 GL_TEXTURE_CUBE_MAP_POSITIVE_X | 右
GL_TEXTURE_CUBE_MAP_NEGATIVE_X | 左 GL_TEXTURE_CUBE_MAP_NEGATIVE_X | 左
@@ -40,10 +44,9 @@ GL_TEXTURE_CUBE_MAP_NEGATIVE_Y | 下
GL_TEXTURE_CUBE_MAP_POSITIVE_Z | 后 GL_TEXTURE_CUBE_MAP_POSITIVE_Z | 后
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z | 前 GL_TEXTURE_CUBE_MAP_NEGATIVE_Z | 前
和很多OpenGL其他枚举一样对应的int值都是线性增加的所以我们有一个纹理位置的数组或vector以 GL_TEXTURE_CUBE_MAP_POSITIVE_X为起始来对它们进行遍历迭代枚举值加1,这样循环所有的纹理目标效率较高: 和很多OpenGL其他枚举一样对应的int值都是连续增加的所以我们有一个纹理位置的数组或vector`GL_TEXTURE_CUBE_MAP_POSITIVE_X`为起始来对它们进行遍历,每迭代枚举值加 `1`,这样循环所有的纹理目标效率较高:
```c++ ```c++
int width,height; int width,height;
unsigned char* image; unsigned char* image;
for(GLuint i = 0; i < textures_faces.size(); i++) for(GLuint i = 0; i < textures_faces.size(); i++)
@@ -54,29 +57,30 @@ for(GLuint i = 0; i < textures_faces.size(); i++)
0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image
); );
} }
``` ```
这儿我们有个vector叫textures_faces它包含cubemap所需所有纹理位置,并且以上表所列的顺序给出。它为每个当前绑定的cubemp的每个面生成一个纹理。 这儿我们有个vector叫`textures_faces`它包含cubemap所各个纹理的文件路径,并且以上表所列的顺序排列。它为每个当前绑定的cubemp的每个面生成一个纹理。
由于cubemap和其他纹理没什么不同我们也要定义它的放置方式和过滤方式: 由于cubemap和其他纹理没什么不同我们也要定义它的环绕方式和过滤方式:
```c++ ```c++
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 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_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_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_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
``` ```
在片段着色器中我们也必须使用一个不同的采样器——samplerCube用它来从texture函数中采样但是这次使用的是一个vec3方向向量取代vec2。下面是一个片段着色器使用了cubemap的例子 别被 `GL_TEXTURE_WRAP_R`吓到它只是简单的设置了纹理的R坐标R坐标对应于纹理的第三个维度就像位置的z一样。我们把放置方式设置为 `GL_CLAMP_TO_EDGE` ,由于纹理坐标在两个面之间,所以可能并不能触及哪个面(由于硬件限制),因此使用 `GL_CLAMP_TO_EDGE` 后OpenGL会返回它们的边界的值尽管我们可能在两个两个面中间进行的采样。
在绘制物体之前将使用cubemap而在渲染前我们要激活相应的纹理单元并绑定到cubemap上这和普通的2D纹理没什么区别。
在片段着色器中,我们也必须使用一个不同的采样器——**samplerCube**,用它来从`texture`函数中采样,但是这次使用的是一个`vec3`方向向量,取代`vec2`。下面是一个片段着色器使用了cubemap的例子
```c++ ```c++
in vec3 textureDir; // 用一个三维方向向量来表示Cubemap纹理的坐标
in vec3 textureDir; // Direction vector representing a 3D texture coordinate uniform samplerCube cubemap; // Cubemap纹理采样器
uniform samplerCube cubemap; // Cubemap texture sampler
void main() void main()
{ {
@@ -84,33 +88,29 @@ void main()
} }
``` ```
看起来不错但是何必这么做呢因为恰巧使用cubemap可以简单的实现很多有意思的技术。其中之一便是天空盒skybox 看起来不错但是何必这么做呢因为恰巧使用cubemap可以简单的实现很多有意思的技术。其中之一便是著名的**天空盒(Skybox)**
### 天空盒(Skybox) ## 天空盒(Skybox)
天空盒是一个包裹整个场景的立方体它由6个图像构成一个环绕的环境给玩家一种他所在的场景比实际的要大得多的幻觉。比如有些在视频游戏中使用的天空盒的图像是群山、白云或者满天繁星。比如下面的夜空繁星的图像就来自《上古卷轴》3 天空盒是一个包裹整个场景的立方体它由6个图像构成一个环绕的环境给玩家一种他所在的场景比实际的要大得多的幻觉。比如有些在视频游戏中使用的天空盒的图像是群山、白云或者满天繁星。比如下面的夜空繁星的图像就来自《上古卷轴》
别被GL_TEXTURE_WRAP_R吓到它只是简单的设置了纹理的R坐标R坐标对应于纹理的第三个维度就像位置的z一样。我们把放置方式设置为GL_CLAMP_TO_EDGE由于纹理坐标在两个面之间所以可能并不能触及哪个面由于硬件限制因此使用GL_CLAMP_TO_EDGE后OpenGL会返回它们的边界的值尽管我们可能在两个两个面中间进行的采样。
在绘制物体之前将使用cubemap而在渲染前我们要激活相应的纹理单元并绑定到cubemap上这和普通的2D纹理没什么区别。
![](http://learnopengl.com/img/advanced/cubemaps_morrowind.jpg) ![](http://learnopengl.com/img/advanced/cubemaps_morrowind.jpg)
你现在可能已经猜到cubemap完全满足天空盒的要求我们有一个立方体它有6个面每个面需要一个贴图。上图中使用了几个夜空的图片给予玩家一种置身广袤宇宙的感觉可实际上他还是在一个小盒子之中。 你现在可能已经猜到cubemap完全满足天空盒的要求我们有一个立方体它有6个面每个面需要一个贴图。上图中使用了几个夜空的图片给予玩家一种置身广袤宇宙的感觉可实际上他还是在一个小盒子之中。
网上有很多这样的天空盒的资源。这个网站就提供了很多。这些天空盒图像通常有下面的样式: 网上有很多这样的天空盒的资源。[这个网站](http://www.custommapmakers.org/skyboxes.php)就提供了很多。这些天空盒图像通常有下面的样式:
![](http://learnopengl.com/img/advanced/cubemaps_skybox.png) ![](http://learnopengl.com/img/advanced/cubemaps_skybox.png)
如果你把这6个面折叠到一个立方体中你机会获得模拟了一个巨大的风景的立方体。有些资源所提供的天空盒比如这个例子6个图是连在一起的你必须手工它们切割出来不过大多数情况它们都是6个单独的纹理图像。 如果你把这6个面折叠到一个立方体中你机会获得模拟了一个巨大的风景的立方体。有些资源所提供的天空盒比如这个例子6个图是连在一起的你必须手工它们切割出来不过大多数情况它们都是6个单独的纹理图像。
这个细致(高精度)的天空盒就是我们将在场景中使用的那个,你可以在这里下载。 这个细致(高精度)的天空盒就是我们将在场景中使用的那个,你可以[在这里下载](http://learnopengl.com/img/textures/skybox.rar)
#### 加载一个天空盒 ### 加载一个天空盒
由于天空盒实际上就是一个cubemap加载天空盒和之前我们看到的没什么大的不同。为了加载天空盒我们将使用下面的函数它接收一个包含6个纹理位置的vector 由于天空盒实际上就是一个cubemap加载天空盒和之前我们加载cubemap的没什么大的不同。为了加载天空盒我们将使用下面的函数它接收一个包含6个纹理文件路径的vector
```c++ ```c++
GLuint loadCubemap(vector<const GLchar*> faces) GLuint loadCubemap(vector<const GLchar*> faces)
@@ -157,13 +157,13 @@ faces.push_back("front.jpg");
GLuint cubemapTexture = loadCubemap(faces); GLuint cubemapTexture = loadCubemap(faces);
``` ```
现在我们已经用cubemapTexture作为id把天空盒加载为cubemap。我们现在可以把它绑定到一个立方体来替换不完美的clear color在前面的所有教程中这个东西做背景已经很久了。 现在我们已经用`cubemapTexture`作为id把天空盒加载为cubemap。我们现在可以把它绑定到一个立方体来替换不完美的`clear color`,在前面的所有教程中这个东西做背景已经很久了。
#### 天空盒的显示 ### 天空盒的显示
因为天空盒绘制在了一个立方体上我们还需要另一个VAO、VBO以及一组全新的顶点和任何其他物体一样。你可以从这里获得顶点数据。 因为天空盒绘制在了一个立方体上我们还需要另一个VAO、VBO以及一组全新的顶点和任何其他物体一样。你可以[从这里获得顶点数据](http://learnopengl.com/code_viewer.php?code=advanced/cubemaps_skybox_data)
cubemap用于给3D立方体帖上纹理可以用立方体的位置作为纹理坐标进行采样。当一个立方体的中心位于原点(000)的时候,它的每一个位置向量也就是以原点为起点的方向向量。这个方向向量就是我们要得到的立方体某个位置的相应纹理值。出于这个理由,我们只需要提供位置向量,而无需纹理坐标。为了渲染天空盒,我们需要一组新着色器,它们不会太复杂。因为我们只有一个顶点属性,顶点着色器非常简单: cubemap用于给3D立方体帖上纹理可以用立方体的位置作为纹理坐标进行采样。当一个立方体的中心位于原点(000)的时候,它的每一个位置向量也就是以原点为起点的方向向量。这个方向向量就是我们要得到的立方体某个位置的相应纹理值。出于这个理由,我们只需要提供位置向量,而无需纹理坐标。为了渲染天空盒,我们需要一组新着色器,它们不会太复杂。因为我们只有一个顶点属性,顶点着色器非常简单:
@@ -197,7 +197,7 @@ void main()
} }
``` ```
片段着色器比较明了我们把顶点属性中的位置向量作为纹理的方向向量使用它们从cubemap采样纹理值。渲染天空盒现在很简单我们有了一个cubemap纹理我们简单绑定cubemap纹理天空盒就自动地用天空盒的cubemap填充了。为了绘制天空盒我们将把它作为场景中第一个绘制的物体并且关闭深度写入。这样天空盒才能成为所有其他物体的背景来绘制出来。 片段着色器比较明了我们把顶点属性中的位置向量作为纹理的方向向量使用它们从cubemap采样纹理值。渲染天空盒现在很简单我们有了一个cubemap纹理我们简单绑定cubemap纹理天空盒就自动地用天空盒的cubemap填充了。为了绘制天空盒我们将把它作为场景中第一个绘制的物体并且关闭深度写入。这样天空盒才能成为所有其他物体的背景来绘制出来。
```c++ ```c++
@@ -226,13 +226,13 @@ glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
尝试用不同的天空盒实验,看看它们对场景有多大影响。 尝试用不同的天空盒实验,看看它们对场景有多大影响。
#### 优化 ### 优化
现在我们在渲染场景中的其他物体之前渲染了天空盒。这么做没错但是不怎么高效。如果我们先渲染了天空盒那么我们就是在为每一个屏幕上的像素运行片段着色器即使天空盒只有部分在显示着fragment可以使用前置深度测试early depth testing简单地被丢弃这样就节省了我们宝贵的带宽。 现在我们在渲染场景中的其他物体之前渲染了天空盒。这么做没错但是不怎么高效。如果我们先渲染了天空盒那么我们就是在为每一个屏幕上的像素运行片段着色器即使天空盒只有部分在显示着fragment可以使用前置深度测试early depth testing简单地被丢弃这样就节省了我们宝贵的带宽。
所以最后渲染天空盒就能够给我们带来轻微的性能提升。采用这种方式,深度缓冲被全部物体的深度值完全填充,所以我们只需要渲染通过前置深度测试的那部分天空的fragment就行了而且能显著减少片段着色器的调用。问题是天空盒是个1×1×1的立方体极有可能会渲染失败因为极有可能通不过深度测试。简单地不用深度测试渲染它也不是解决方案这是因为天空盒会在之后覆盖所有的场景中其他物体。我们需要耍个花招让深度缓冲相信天空盒的深度缓冲有着最大深度值1.0,如此只要有个物体存在深度测试就会失败,看似物体就在它前面了。 所以最后渲染天空盒就能够给我们带来轻微的性能提升。采用这种方式,深度缓冲被全部物体的深度值完全填充,所以我们只需要渲染通过前置深度测试的那部分天空的片段就行了而且能显著减少片段着色器的调用。问题是天空盒是个1×1×1的立方体极有可能会渲染失败因为极有可能通不过深度测试。简单地不用深度测试渲染它也不是解决方案这是因为天空盒会在之后覆盖所有的场景中其他物体。我们需要耍个花招让深度缓冲相信天空盒的深度缓冲有着最大深度值1.0,如此只要有个物体存在深度测试就会失败,看似物体就在它前面了。
在坐标系教程中我们说过透视除法perspective division是在顶点着色器运行之后执行的把gl_Position的xyz坐标除以w元素。我们从深度测试教程了解到除法结果的z元素等于顶点的深度值。利用这个信息我们可以把输出位置的z元素设置为它的w元素这样就会导致z元素等于1.0了因为当透视除法应用后它的z元素转换为w/w = 1.0 在坐标系教程中我们说过透视除法perspective division是在顶点着色器运行之后执行的`gl_Position`的xyz坐标除以w元素。我们从深度测试教程了解到除法结果的z元素等于顶点的深度值。利用这个信息我们可以把输出位置的z元素设置为它的w元素这样就会导致z元素等于1.0了因为当透视除法应用后它的z元素转换为w/w = 1.0
```c++ ```c++
void main() void main()
@@ -245,19 +245,19 @@ void main()
最终标准化设备坐标就总会有个与1.0相等的z值了1.0就是深度值的最大值。只有在没有任何物体可见的情况下天空盒才会被渲染(只有通过深度测试才渲染,否则假如有任何物体存在,就不会被渲染,只去渲染物体)。 最终标准化设备坐标就总会有个与1.0相等的z值了1.0就是深度值的最大值。只有在没有任何物体可见的情况下天空盒才会被渲染(只有通过深度测试才渲染,否则假如有任何物体存在,就不会被渲染,只去渲染物体)。
我们必须改变一下深度方程把它设置为GL_LEQUAL原来默认的是GL_LESS。深度缓冲会为天空盒用1.0这个值填充深度缓冲,所以我们需要保证天空盒是使用小等于深度缓冲来通过深度测试的,而不是小于。 我们必须改变一下深度方程,把它设置为`GL_LEQUAL`,原来默认的是`GL_LESS`。深度缓冲会为天空盒用1.0这个值填充深度缓冲,所以我们需要保证天空盒是使用小等于深度缓冲来通过深度测试的,而不是小于。
你可以在这里找到优化过的版本的源码。 你可以在这里找到优化过的版本的[源码](http://learnopengl.com/code_viewer.php?code=advanced/cubemaps_skybox_optimized)
#### 环境映射 ### 环境映射
我们现在有了一个把整个环境映射到为一个单独纹理的对象我们利用这个信息能做的不仅是天空盒。使用带有场景环境的cubemap我们还可以让物体有一个反射或折射属性。像这样使用了环境cubemap的技术叫做环境贴图技术其中最重要的两个是反射和折射 我们现在有了一个把整个环境映射到为一个单独纹理的对象我们利用这个信息能做的不仅是天空盒。使用带有场景环境的cubemap我们还可以让物体有一个反射或折射属性。像这样使用了环境cubemap的技术叫做**环境贴图技术**,其中最重要的两个是**反射(reflection)**和**折射(refraction)**
##### 反射 #### 反射(reflection)
凡是是一个物体(或物体的某部分)反射他周围的环境的属性,比如物体的颜色多少有些等于它周围的环境,这要基于观察者的角度。例如一个镜子是一个反射物体:它会基于观察者的角度泛着它周围的环境。 凡是是一个物体(或物体的某部分)反射他周围的环境的属性,比如物体的颜色多少有些等于它周围的环境,这要基于观察者的角度。例如一个镜子是一个反射物体:它会基于观察者的角度泛着它周围的环境。
反射的基本思路不难。下麦呢的图片展示了我们如何计算反射向量然后使用这个向量去从一个cubemap中采样 反射的基本思路不难。下展示了我们如何计算反射向量然后使用这个向量去从一个cubemap中采样
![](http://learnopengl.com/img/advanced/cubemaps_reflection_theory.png) ![](http://learnopengl.com/img/advanced/cubemaps_reflection_theory.png)
@@ -282,7 +282,7 @@ void main()
} }
``` ```
我们先来计算观察/摄像机方向向量I然后使用它来计算反射向量R接着我们用R从天空盒cubemap采样。要注意的是我们有了fragment的插值Normal和Position变量所以我们需要修正顶点着色器适应它。 我们先来计算观察/摄像机方向向量I然后使用它来计算反射向量R接着我们用R从天空盒cubemap采样。要注意的是我们有了片段的插值Normal和Position变量所以我们需要修正顶点着色器适应它。
```c++ ```c++
#version 330 core #version 330 core
@@ -304,9 +304,9 @@ void main()
} }
``` ```
我们用了法线向量,所以我们打算使用一个法线矩阵normal matrix变换它们。Position输出的向量是一个世界空间位置向量。顶点着色器输出的Position用来在片段着色器计算观察方向向量。 我们用了法线向量,所以我们打算使用一个**法线矩阵(normal matrix)**变换它们。`Position`输出的向量是一个世界空间位置向量。顶点着色器输出的`Position`用来在片段着色器计算观察方向向量。
因为我们使用法线你还得更新顶点数据更新属性指针。还要确保设置cameraPos的uniform。 因为我们使用法线,你还得更新顶点数据,更新属性指针。还要确保设置`cameraPos`的uniform。
然后在渲染箱子前我们还得绑定cubemap纹理 然后在渲染箱子前我们还得绑定cubemap纹理
@@ -323,15 +323,15 @@ glBindVertexArray(0);
你可以[从这里找到全部源代码](http://learnopengl.com/code_viewer.php?code=advanced/cubemaps_reflection)。 你可以[从这里找到全部源代码](http://learnopengl.com/code_viewer.php?code=advanced/cubemaps_reflection)。
当反射应用于整个物体之上的时候,物体看上去就像有一个像钢和铬这种高反射材质。如果我们加载模型加载教程中的纳米铠甲模型,我们就会获得一个铬金属制铠甲: 当反射应用于整个物体之上的时候,物体看上去就像有一个像钢和铬这种高反射材质。如果我们加载[模型教程](http://learnopengl-cn.readthedocs.org/zh/latest/03%20Model%20Loading/03%20Model/)中的纳米铠甲模型,我们就会获得一个铬金属制铠甲:
![](http://learnopengl.com/img/advanced/cubemaps_reflection_nanosuit.png) ![](http://learnopengl.com/img/advanced/cubemaps_reflection_nanosuit.png)
看起来挺惊艳但是现实中大多数模型都不是完全反射的。我们可以引进反射贴图reflection map来使模型有另一层细节。和diffuse、specular贴图一样我们可以从反射贴图上采样来决定fragment的反射率。使用反射贴图我们还可以决定模型的哪个部分有反射能力以及强度是多少。本节的练习中要由你来在我们早期创建的模型加载器引入反射贴图这回极大的提升纳米服模型的细节。 看起来挺惊艳但是现实中大多数模型都不是完全反射的。我们可以引进反射贴图reflection map来使模型有另一层细节。和diffuse、specular贴图一样我们可以从反射贴图上采样来决定fragment的反射率。使用反射贴图我们还可以决定模型的哪个部分有反射能力以及强度是多少。本节的练习中要由你来在我们早期创建的模型加载器引入反射贴图这回极大的提升纳米服模型的细节。
#### 折射 #### 折射(refraction)
环境映射的另一个形式叫做折射,它和反射差不多。折射是光线通过特定材质对光线方向的改变。我们通常看到像水一样的表面,光线并不是直接通过的,而是让光线弯曲了一点。它看起来像你把半只手伸进水里的效果。 环境映射的另一个形式叫做折射,它和反射差不多。折射是光线通过特定材质对光线方向的改变。我们通常看到像水一样的表面,光线并不是直接通过的,而是让光线弯曲了一点。它看起来像你把半只手伸进水里的效果。
折射遵守[斯涅尔定律](http://en.wikipedia.org/wiki/Snell%27s_law),使用环境贴图看起来就像这样: 折射遵守[斯涅尔定律](http://en.wikipedia.org/wiki/Snell%27s_law),使用环境贴图看起来就像这样:
@@ -367,15 +367,7 @@ void main()
通过改变折射指数你可以创建出完全不同的视觉效果。编译运行应用,结果也不是太有趣,因为我们只是用了一个普通箱子,这不能显示出折射的效果,看起来像个放大镜。使用同一个着色器,纳米服模型却可以展示出我们期待的效果:玻璃制物体。 通过改变折射指数你可以创建出完全不同的视觉效果。编译运行应用,结果也不是太有趣,因为我们只是用了一个普通箱子,这不能显示出折射的效果,看起来像个放大镜。使用同一个着色器,纳米服模型却可以展示出我们期待的效果:玻璃制物体。
```c++ ![](http://learnopengl.com/img/advanced/cubemaps_refraction.png)
void main()
{
float ratio = 1.00 / 1.52;
vec3 I = normalize(Position - cameraPos);
vec3 R = refract(I, normalize(Normal), ratio);
color = texture(skybox, R);
}
```
你可以向想象一下,如果将光线、反射、折射和顶点的移动合理的结合起来就能创造出漂亮的水的图像。一定要注意,出于物理精确的考虑当光线离开物体的时候还要再次进行折射;现在我们简单的使用了单边(一次)折射,大多数目的都可以得到满足。 你可以向想象一下,如果将光线、反射、折射和顶点的移动合理的结合起来就能创造出漂亮的水的图像。一定要注意,出于物理精确的考虑当光线离开物体的时候还要再次进行折射;现在我们简单的使用了单边(一次)折射,大多数目的都可以得到满足。
@@ -389,6 +381,18 @@ void main()
### 练习 ## 练习
//TODO 尝试在模型加载中引进反射贴图,你将再次得到很大视觉效果的提升。这其中有几点需要注意:
- Assimp并不支持反射贴图我们可以使用环境贴图的方式将反射贴图从`aiTextureType_AMBIENT`类型中来加载反射贴图的材质。
- 我匆忙地使用反射贴图来作为镜面反射的贴图,而反射贴图并没有很好的映射在模型上:)。
- 由于加载模型已经占用了3个纹理单元因此你要绑定天空盒到第4个纹理单元上这样才能在同一个着色器内从天空盒纹理中取样。
You can find the solution source code here together with the updated model and mesh class. The shaders used for rendering the reflection maps can be found here: vertex shader and fragment shader.
你可以在此获取解决方案的[源代码](http://learnopengl.com/code_viewer.php?code=advanced/cubemaps-exercise1),这其中还包括升级过的[Model](http://learnopengl.com/code_viewer.php?code=advanced/cubemaps-exercise1-model)和[Mesh](http://learnopengl.com/code_viewer.php?code=advanced/cubemaps-exercise1-mesh)类,还有用来绘制反射贴图的[顶点着色器](http://learnopengl.com/code_viewer.php?code=advanced/cubemaps-exercise1-vertex)和[片段着色器](http://learnopengl.com/code_viewer.php?code=advanced/cubemaps-exercise1-fragment)。
如果你一切都做对了,那你应该看到和下图类似的效果:
![](http://learnopengl.com/img/advanced/cubemaps_reflection_map.png)

View File

@@ -1,20 +1,26 @@
## 高级数据 # 高级数据
本文作者JoeyDeVries由Django翻译自http://learnopengl.com 原文 | [Advanced Data](http://learnopengl.com/#!Advanced-OpenGL/Advanced-Data)
---|---
作者 | JoeyDeVries
翻译 | [Django](http://bullteacher.com/)
校对 | [Geequlim](http://geequlim.com)
## 缓冲数据写入
我们在OpenGL中大量使用缓冲来储存数据已经有一会儿了。有一些有趣的方式来操纵缓冲也有一些有趣的方式通过纹理来向着色器传递大量数据。本教程中我们会讨论一些更加有意思的缓冲函数以及如何使用纹理对象来储存大量数据教程中纹理部分还没写 我们在OpenGL中大量使用缓冲来储存数据已经有一会儿了。有一些有趣的方式来操纵缓冲也有一些有趣的方式通过纹理来向着色器传递大量数据。本教程中我们会讨论一些更加有意思的缓冲函数以及如何使用纹理对象来储存大量数据教程中纹理部分还没写
OpenGL中缓冲知识一个管理一块儿内存区域的对象除此没有更多点的了。当把缓冲绑定到一个特定缓冲对象的时候我们就给缓冲赋予了一个特殊的意义。当我们绑定到GL_ARRAY_BUFFER的时候这个缓冲就是一个顶点数组缓冲我们也可以简单地绑定到GL_ELEMENT_ARRAY_BUFFER。OpenGL内部为每个目标target储存一个缓冲基于目标来处理不同的缓冲。 OpenGL中缓冲只是一块儿内存区域的对象,除此没有更多点的了。当把缓冲绑定到一个特定缓冲对象的时候,我们就给缓冲赋予了一个特殊的意义。当我们绑定到`GL_ARRAY_BUFFER`的时候,这个缓冲就是一个顶点数组缓冲,我们也可以简单地绑定到`GL_ELEMENT_ARRAY_BUFFER`。OpenGL内部为每个目标target储存一个缓冲基于目标来处理不同的缓冲。
到目前为止,我们已经使用glBufferData函数填充缓冲对象管理的内存,这个函数分了一块内存空间,然后把数据存入内存。如果我们向它的数据(data这个参数传递的是NULL只会分类内存,而不会填充它。如果我们先打算开辟一些内存,稍后回到这个缓冲一点一点的填充内存的时候很有用。 到目前为止,我们使用`glBufferData`函数填充缓冲对象管理的内存,这个函数分了一块内存空间,然后把数据存入其中。如果我们向它的`data`这个参数传递的是NULL那么OpenGL只会帮我们分配内存,而不会填充它。如果我们先打算开辟一些内存,稍后回到这个缓冲一点一点的填充数据,有些时候很有用。
我们还可以调用glBufferSubData函数填充特定区域的缓冲而不是一次填充整个缓冲。这个函数需要一个缓冲目标target一个偏移量offset数据的大小以及数据本身作为参数。这个函数新的功能是我们可以给它一个偏移量offset来指定我们打算填充缓冲的位置与起始位置之间的偏移量。这样我们就可以插入/更新指定区域的缓冲内存空间了。一定要记住缓冲要有足够的内存分配所以在调用glBufferSubData之前调用glBufferData是必须的。 我们还可以调用`glBufferSubData`函数填充特定区域的缓冲而不是一次填充整个缓冲。这个函数需要一个缓冲目标target一个偏移量offset数据的大小以及数据本身作为参数。这个函数新的功能是我们可以给它一个偏移量offset来指定我们打算填充缓冲的位置与起始位置之间的偏移量。这样我们就可以插入/更新指定区域的缓冲内存空间了。一定要确保修改的缓冲要有足够的内存分配,所以在调用`glBufferSubData`之前,调用`glBufferData`是必须的。
```c++ ```c++
glBufferSubData(GL_ARRAY_BUFFER, 24, sizeof(data), &data); // 范围: [24, 24 + sizeof(data)] glBufferSubData(GL_ARRAY_BUFFER, 24, sizeof(data), &data); // 范围: [24, 24 + sizeof(data)]
``` ```
把数据传进缓冲另一个方式是缓冲内存请求一个指针你自己直接把数据复制到缓冲中。调用glMapBuffer函数OpenGL会返回一个当前绑定缓冲的内存的地址供我们操作 把数据传进缓冲另一个方式是缓冲内存请求一个指针,你自己直接把数据复制到缓冲中。调用`glMapBuffer`函数OpenGL会返回一个当前绑定缓冲的内存的地址供我们操作
```c++ ```c++
float data[] = { float data[] = {
@@ -23,37 +29,37 @@ float data[] = {
}; };
glBindBuffer(GL_ARRAY_BUFFER, buffer); glBindBuffer(GL_ARRAY_BUFFER, buffer);
// Get pointer // 获取当前绑定缓存buffer的内存地址
void* ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); void* ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
// Now copy data into memory // 向缓冲中写入数据
memcpy(ptr, data, sizeof(data)); memcpy(ptr, data, sizeof(data));
// Make sure to tell OpenGL we're done with the pointer // 完成够别忘了告诉OpenGL我们不再需要它了
glUnmapBuffer(GL_ARRAY_BUFFER); glUnmapBuffer(GL_ARRAY_BUFFER);
``` ```
调用glUnmapBuffer函数可以告诉OpenGL我们已经用完指针了OpenGL会知道你已经做完了。通过解映射unmapping指针会不再可用如果OpenGL可以把你的数据映射到缓冲上就会返回GL_TRUE。 调用`glUnmapBuffer`函数可以告诉OpenGL我们已经用完指针了OpenGL会知道你已经做完了。通过解映射unmapping指针会不再可用如果OpenGL可以把你的数据映射到缓冲上就会返回`GL_TRUE`
把数据直接映射到冲上使用glMapBuffer很有用因为不用把它储存在临时内存里。你可以从文件读取数据然后直接复制到缓冲的内存里。 把数据直接映射到冲上使用`glMapBuffer`很有用,因为不用把它储存在临时内存里。你可以从文件读取数据然后直接复制到缓冲的内存里。
### 分批处理顶点属性 ## 分批处理顶点属性
使用glVertexAttribPointer函数我们可以指定缓冲内容的顶点数组的属性layout(布局)。顶点数组缓冲,使我们可以交叉属性也就是说我们把每个顶点的位置、法线、纹理坐标放在彼此挨着的地方。现在我们了解了更多的缓冲的内容,我们可以采取另一种方式了。 使用`glVertexAttribPointer`函数可以指定缓冲内容的顶点数组的属性的布局(layout)。我们已经知道,通过使用顶点属性指针我们可以交叉属性也就是说我们可以把每个顶点的位置、法线、纹理坐标放在彼此挨着的地方。现在我们了解了更多的缓冲的内容,可以采取另一种方式了。
我们可以做的是把每种类型的属性的所有向量数据批量保存在一个布局而不是交叉布局。与交叉布局123123123123不同我们采取批量方式111122223333。 我们可以做的是把每种类型的属性的所有向量数据批量保存在一个布局而不是交叉布局。与交叉布局123123123123不同我们采取批量方式111122223333。
当从文件加载顶点数据时你通常获取一个位置数组一个法线数组和一个纹理坐标数组。需要花点力气才能把它们结合为交叉数据。使用glBufferSubData可以简单的实现分批处理方式 当从文件加载顶点数据时你通常获取一个位置数组,一个法线数组和一个纹理坐标数组。需要花点力气才能把它们结合为交叉数据。使用`glBufferSubData`可以简单的实现分批处理方式:
```c++ ```c++
GLfloat positions[] = { ... }; GLfloat positions[] = { ... };
GLfloat normals[] = { ... }; GLfloat normals[] = { ... };
GLfloat tex[] = { ... }; GLfloat tex[] = { ... };
// Fill buffer // 填充缓冲
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(positions), &positions); glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(positions), &positions);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions), sizeof(normals), &normals); glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions), sizeof(normals), &normals);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions) + sizeof(normals), sizeof(tex), &tex); glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions) + sizeof(normals), sizeof(tex), &tex);
``` ```
这样我们可以把属性数组当作一个整体直接传输给缓冲不需要再处理它们了。我们还可以把它们结合为一个更大的数组然后使用glBufferData立即直接填充它不过对于这项任务使用glBufferSubData是更好的选择。 这样我们可以把属性数组当作一个整体直接传输给缓冲,不需要再处理它们了。我们还可以把它们结合为一个更大的数组然后使用`glBufferData`立即直接填充它,不过对于这项任务使用`glBufferSubData`是更好的选择。
我们还要更新顶点属性指针来反应这些改变: 我们还要更新顶点属性指针来反应这些改变:
@@ -64,23 +70,23 @@ glVertexAttribPointer(
2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid*)(sizeof(positions) + sizeof(normals))); 2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid*)(sizeof(positions) + sizeof(normals)));
``` ```
注意stride参数等于顶点属性的大小由于下一个顶点属性向量可以在它的后面3或2的元素那儿找到。 注意,`stride`参数等于顶点属性的大小,由于同类型的属性是连续储存的,所以下一个顶点属性向量可以在它的后面3或2的元素那儿找到。
这是我们有了另一种设置和指定顶点属性的方式。使用哪个方式对OpenGL来说也不会有立竿见影的效果这只是一种采用更加组织化的方式去设置顶点属性。选用哪种方式取决于你的偏好和应用类型。 这是我们有了另一种设置和指定顶点属性的方式。使用哪个方式对OpenGL来说也不会有立竿见影的效果这只是一种采用更加组织化的方式去设置顶点属性。选用哪种方式取决于你的偏好和应用类型。
### 复制缓冲 ## 复制缓冲
当你的缓冲被数据填充以后你可能打算让其他缓冲能分享这些数据或者打算把缓冲的内容复制到另一个缓冲里。glCopyBufferSubData函数让我们能够相对容易地把一个缓冲的数据复制到另一个缓冲里。函数的原型是 当你的缓冲被数据填充以后,你可能打算让其他缓冲能分享这些数据或者打算把缓冲的内容复制到另一个缓冲里。`glCopyBufferSubData`函数让我们能够相对容易地把一个缓冲的数据复制到另一个缓冲里。函数的原型是:
```c++ ```c++
void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset, GLintptr writeoffset, GLsizeiptr size); void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset, GLintptr writeoffset, GLsizeiptr size);
``` ```
readtargetwritetarget参数是复制的来源和目的的缓冲目标。例如我们可以从一个VERTEX_ARRAY_BUFFER复制到一个VERTEX_ELEMENT_ARRAY_BUFFER各自指定源和目的的缓冲目标。当前绑定到这些缓冲目标上的缓冲会被影响到。 `readtarget`和`writetarget`参数是复制的来源和目的的缓冲目标。例如我们可以从一个`VERTEX_ARRAY_BUFFER`复制到一个`VERTEX_ELEMENT_ARRAY_BUFFER`,各自指定源和目的的缓冲目标。当前绑定到这些缓冲目标上的缓冲会被影响到。
但如果我们打算读写的两个缓冲都是顶点数组缓冲(译注:GL_VERTEX_ARRAY_BUFFER怎么办?我们不能用一个缓冲目标用两次。于这个理由,这是个例外,OpenGL给了我们另外两个缓冲目标叫做GL_COPY_READ_BUFFERGL_COPY_WRITE_BUFFER。这样我们就可以把我们选择的缓冲用上面二者作为readtargetwritetarget的参数绑定到新的缓冲目标上了。 但如果我们打算读写的两个缓冲都是顶点数组缓冲(`GL_VERTEX_ARRAY_BUFFER`)怎么办?我们不能用一个缓冲作为操作的读取和写入目标次。于这个理由OpenGL给了我们另外两个缓冲目标叫做`GL_COPY_READ_BUFFER`和`GL_COPY_WRITE_BUFFER`。这样我们就可以把我们选择的缓冲,用上面二者作为`readtarget`和`writetarget`的参数绑定到新的缓冲目标上了。
接着glCopyBufferSubData函数会从readoffset处读取的size大小的数据写入到writetarget缓冲的writeoffset位置。下面是一个复制两个顶点数组缓冲的例子 接着`glCopyBufferSubData`函数会从readoffset处读取的size大小的数据写入到writetarget缓冲的writeoffset位置。下面是一个复制两个顶点数组缓冲的例子
```c++ ```c++
GLfloat vertexData[] = { ... }; GLfloat vertexData[] = { ... };
@@ -89,7 +95,7 @@ glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData)); glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));
``` ```
我们也可以把writetarget缓冲绑定为新缓冲目标类型其中之一 我们也可以把`writetarget`缓冲绑定为新缓冲目标类型其中之一:
```c++ ```c++
GLfloat vertexData[] = { ... }; GLfloat vertexData[] = { ... };
@@ -98,4 +104,4 @@ glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
glCopyBufferSubData(GL_ARRAY_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData)); glCopyBufferSubData(GL_ARRAY_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));
``` ```
有了这些额外的关于如何操纵缓冲的知识,我们已经可以以更有意思的方式来使用它们了。当你对OpenGL更熟悉这些新缓冲方法就变得更有用。下个教程中我们会讨论unform缓冲对象彼时我们会充分利用glBufferSubData。 有了这些额外的关于如何操纵缓冲的知识,我们已经可以以更有的方式来使用它们了。当你对OpenGL更熟悉这些新缓冲方法就变得更有用。下个教程中我们会讨论unform缓冲对象彼时我们会充分利用`glBufferSubData`

View File

@@ -1,28 +1,35 @@
## 高级GLSL # 高级GLSL
本文作者JoeyDeVries由Django翻译自http://learnopengl.com 原文 | [Advanced GLSL](http://learnopengl.com/#!Advanced-OpenGL/Advanced-GLSL)
---|---
作者 | JoeyDeVries
翻译 | [Django](http://bullteacher.com/)
校对 | [Geequlim](http://geequlim.com)
这章不会向你展示什么新的功能也不会对你的场景的视觉效果有较大提升。本章多多少少地深入探讨了一些GLSL有意思的方面以及可能在将来能帮助你的技巧。基本来说有些不可不知的内容和功能在你去使用GLSL创建OpenGL应用的时候能让你的生活更轻松。
我们会讨论一些有趣的内建变量、组织着色器输入和输出的新方式以及一个叫做uniform缓冲对象的非常有用的工具 这章不会向你展示什么新的功能也不会对你的场景的视觉效果有较大提升。本文多多少少地深入探讨了一些GLSL有趣的知识它们可能在将来能帮助你。基本来说有些不可不知的内容和功能在你去使用GLSL创建OpenGL应用的时候能让你的生活更轻松
### GLSL的内建变量 我们会讨论一些内建变量、组织着色器输入和输出的新方式以及一个叫做uniform缓冲对象的非常有用的工具。
着色器是很小的如果我们需要从当前着色器以外的别的资源里的数据那么我们就不得不穿给它。我们学过了使用顶点属性、uniform和采样器可以实现这个目标。GLSL有几个以gl_为前缀的变量使我们有一个额外的手段来获取和写入数据。我们已经看到了两个gl_Position和gl_FragCoord前一个是顶点着色器的输出向量后一个是片段着色器的变量 ## GLSL的内建变量
我们会讨论几个有趣的GLSL内建变量并向你解释为什么它们对我们来说很有好处。注意我们不会讨论到GLSL中所有的内建变量因此如果你想看到所有的内建变量还是最好去看[OpenGL的wiki](http://www.opengl.org/wiki/Built-in_Variable_(GLSL) 着色器是很小的如果我们需要从当前着色器以外的别的资源里的数据那么我们就不得不传给它。我们学过了使用顶点属性、uniform和采样器可以实现这个目标。GLSL有几个以**gl\_**为前缀的变量,使我们有一个额外的手段来获取和写入数据。其中两个我们已经打过交道了:`gl_Position``gl_FragCoord`,前一个是顶点着色器的输出向量,后一个是片段着色器的变量
#### 顶点着色器变量 我们会讨论几个有趣的GLSL内建变量并向你解释为什么它们对我们来说很有好处。注意我们不会讨论到GLSL中所有的内建变量因此如果你想看到所有的内建变量还是最好去查看[OpenGL的wiki](http://www.opengl.org/wiki/Built-in_Variable_(GLSL)。
我们已经了解gl_Position是顶点着色器裁切空间输出的位置向量。如果你想让屏幕上渲染出东西gl_Position必须使用。否则我们什么都看不到。 ### 顶点着色器变量
##### gl_PointSize #### gl_Position
我们可以使用的另一个可用于渲染的基本图形primitive是GL_POINTS使用它每个顶点作为一个基本图形被渲染为一个点point。可以glPointSize函数来设置这个点的大小但我们还可以在顶点着色器里影响点的大小 我们已经了解`gl_Position`是顶点着色器裁切空间输出的位置向量。如果你想让屏幕上渲染出东西`gl_Position`必须使用。否则我们什么都看不到
GLSL有一个输出变量叫做gl_PointSize他是一个float变量你可以以像素的方式设置点的高度和宽度。每一个在着色器中描述每个顶点做为点的大小。 #### gl_PointSize
在着色器中影响点的大小默认是关闭的但是如果你打算开启它你需要开启OpenGL的GL_PROGRAM_POINT_SIZE 我们可以使用的另一个可用于渲染的基本图形(primitive)是**GL\_POINTS**,使用它每个顶点作为一个基本图形,被渲染为一个点(point)。可以使用`glPointSize`函数来设置这个点的大小,但我们还可以在顶点着色器里修改点的大小。
GLSL有另一个输出变量叫做`gl_PointSize`,他是一个`float`变量,你可以以像素的方式设置点的高度和宽度。它在着色器中描述每个顶点做为点被绘制出来的大小。
在着色器中影响点的大小默认是关闭的但是如果你打算开启它你需要开启OpenGL的`GL_PROGRAM_POINT_SIZE`
```c++ ```c++
glEnable(GL_PROGRAM_POINT_SIZE); glEnable(GL_PROGRAM_POINT_SIZE);
@@ -40,29 +47,29 @@ void main()
结果是我们绘制的点距离我们越远就越大: 结果是我们绘制的点距离我们越远就越大:
![](http://bullteacher.com/wp-content/uploads/2015/06/advanced_glsl_pointsize.png) ![](http://learnopengl.com/img/advanced/advanced_glsl_pointsize.png)
想象一下,每个顶点表示出来的点的大小的不同,如果用在像粒子生成之类的技术里会挺有意思的。 想象一下,每个顶点表示出来的点的大小的不同,如果用在像粒子生成之类的技术里会挺有意思的。
##### gl_VertexID #### gl_VertexID
gl_Positiongl_PointSize都是输出变量因为它们的值是作为顶点着色器的输出被读取的我们可以向它们写入数据来影响结果。顶点着色器为我们提供了一个有趣的输入变量我们只能从它那里读取这个变量叫做gl_VertexID。 `gl_Position`和`gl_PointSize`都是输出变量,因为它们的值是作为顶点着色器的输出被读取的;我们可以向它们写入数据来影响结果。顶点着色器为我们提供了一个有趣的输入变量,我们只能从它那里读取,这个变量叫做`gl_VertexID`
gl_VertexID是个整型变量它储存着我们绘制的当前顶点的ID。当进行索引渲染indexed rendering使用glDrawElements这个变量保存着当前绘制的顶点的索引。当用的不是索引绘制glDrawArrays这个变量保存的是从渲染开始起直到当前处理的这个顶点的当前顶点编号。 `gl_VertexID`是个整型变量它储存着我们绘制的当前顶点的ID。当进行索引渲染indexed rendering使用`glDrawElements`渲染)时,这个变量保存着当前绘制的顶点的索引。当用的不是索引绘制(`glDrawArrays`)时,这个变量保存的是从渲染开始起直到当前处理的这个顶点的(当前顶点)编号。
尽管目前看似没用,但是我们最好知道我们能获取这样的信息。 尽管目前看似没用,但是我们最好知道我们能获取这样的信息。
#### 片段着色器的变量 ### 片段着色器的变量
在片段着色器中也有一些有趣的变量。GLSL给我们提供了两个有意思的输入变量它们是gl_FragCoordgl_FrontFacing。 在片段着色器中也有一些有趣的变量。GLSL给我们提供了两个有意思的输入变量它们是`gl_FragCoord`和`gl_FrontFacing`
##### gl_FragCoord #### gl_FragCoord
在讨论深度测试的时候我们已经看过gl_FragCoord好几次了因为gl_FragCoord向量的z元素和特定的fragment的深度值相等。然而我们也可以使用这个向量的x和y元素来实现一些有趣的效果。 在讨论深度测试的时候,我们已经看过`gl_FragCoord`好几次了,因为`gl_FragCoord`向量的z元素和特定的fragment的深度值相等。然而我们也可以使用这个向量的x和y元素来实现一些有趣的效果。
gl_FragCoord的x和y元素是这个fragment窗口空间坐标window-space coordinate。它们的起始处是窗口的左下角。如果我们的窗口是800×600的那么一个fragment的窗口空间坐标x的范围就在0到800之间y在0到600之间。 `gl_FragCoord`的x和y元素是当前片段的窗口空间坐标window-space coordinate。它们的起始处是窗口的左下角。如果我们的窗口是800×600的那么一个片段的窗口空间坐标x的范围就在0到800之间y在0到600之间。
我们可以使用片段着色器基于fragment的窗口坐标计算出一个不同的颜色。gl_FragCoord变量的一个常用的方式是与一个不同的fragment计算出来的视频输出进行对比,通常在技术演示中常见。比如我们可以把屏幕分为两个部分,窗口的左侧渲染一个输出,窗口的右边渲染另一个输出。下面是一个基于fragment的窗口坐标的位置的不同输出不同的颜色的片段着色器: 我们可以使用片段着色器基于片段的窗口坐标计算出一个不同的颜色。`gl_FragCoord`变量的一个常用的方式是与一个不同的片段计算出来的视频输出进行对比,通常在技术演示中常见。比如我们可以把屏幕分为两个部分,窗口的左侧渲染一个输出,窗口的右边渲染另一个输出。下面是一个基于片段的窗口坐标的位置的不同输出不同的颜色的片段着色器:
```c++ ```c++
@@ -77,19 +84,19 @@ void main()
因为窗口的宽是800当一个像素的x坐标小于400那么它一定在窗口的左边这样我们就让物体有个不同的颜色。 因为窗口的宽是800当一个像素的x坐标小于400那么它一定在窗口的左边这样我们就让物体有个不同的颜色。
![](http://bullteacher.com/wp-content/uploads/2015/06/advanced_glsl_fragcoord.png) ![](http://learnopengl.com/img/advanced/advanced_glsl_frontfacing.png)
我们现在可以计算出两个完全不同的片段着色器结果,每个显示在窗口的一端。这对于测试不同的光照技术很有好处。 我们现在可以计算出两个完全不同的片段着色器结果,每个显示在窗口的一端。这对于测试不同的光照技术很有好处。
gl_FrontFacing #### gl_FrontFacing
片段着色器另一个有意思的输入变量是gl_FrontFacing变量。在面剔除教程中我们提到过OpenGL可以根据顶点绘制顺序弄清楚一个面是正面还是背面。如果我们不适用面剔除那么gl_FrontFacing变量能告诉我们当前fragment是一个正面的一部分还是背面的一部分。然后我们可以决定做一些事情,比如为正面计算出不同的颜色。 片段着色器另一个有意思的输入变量是`gl_FrontFacing`变量。在面剔除教程中我们提到过OpenGL可以根据顶点绘制顺序弄清楚一个面是正面还是背面。如果我们不适用面剔除那么`gl_FrontFacing`变量能告诉我们当前片段是某个正面的一部分还是背面的一部分。然后我们可以决定做一些事情,比如为正面计算出不同的颜色。
gl_FrontFacing变量是一个布尔值如果fragment是正面的一部分那么就是true否则就是false。这样我们可以创建一个立方体里面和外面使用不同的纹理 `gl_FrontFacing`变量是一个布尔值,如果当前片段是正面的一部分那么就是true否则就是false。这样我们可以创建一个立方体里面和外面使用不同的纹理
``c++ ```c++
#version 330 core #version 330 core
out vec4 color; out vec4 color;
in vec2 TexCoords; in vec2 TexCoords;
@@ -110,23 +117,23 @@ void main()
![](http://bullteacher.com/wp-content/uploads/2015/06/advanced_glsl_frontfacing.png) ![](http://bullteacher.com/wp-content/uploads/2015/06/advanced_glsl_frontfacing.png)
注意,如果你开启了面剔除,你就看不到箱子里面有任何东西了,所以再使用gl_FrontFacing毫无意义。 注意,如果你开启了面剔除,你就看不到箱子里面有任何东西了,所以此时使用`gl_FrontFacing`毫无意义。
##### gl_FragDepth #### gl_FragDepth
输入变量gl_FragCoord让我们可以读得当前fragment的窗口空间坐标和深度值,但是它是只读的。我们不能影响到这个fragment的窗口屏幕坐标但是可以设置这个像素的深度值。GLSL给我们提供了一个叫做gl_FragDepth的变量我们可以用它在着色器中遂舍之像素的深度值。 输入变量`gl_FragCoord`让我们可以读得当前片段的窗口空间坐标和深度值,但是它是只读的。我们不能影响到这个片段的窗口屏幕坐标但是可以设置这个像素的深度值。GLSL给我们提供了一个叫做`gl_FragDepth`的变量,我们可以用它在着色器中遂舍之像素的深度值。
为了在着色器中设置深度值我们简单的写一个0.0到1.0之间的float数赋值给这个输出变量 为了在着色器中设置深度值我们简单的写一个0.0到1.0之间的float数赋值给这个输出变量
```c++ ```c++
gl_FragDepth = 0.0f; // This fragment now has a depth value of 0.0f gl_FragDepth = 0.0f; //现在片段的深度值被设为0
``` ```
如果着色器中没有像gl_FragDepth变量写入什么它就会自动采用gl_FragCoord.z的值。 如果着色器中没有像`gl_FragDepth`变量写入,它就会自动采用`gl_FragCoord.z`的值。
我们自己设置深度值有一个显著缺点因为只要我们在片段着色器中对gl_FragDepth写入什么OpenGL就会关闭所有的前置深度测试。它被关闭的原因是在我们运行片段着色器之前OpenGL搞不清像素的深度值因为片段着色器可能会完全改变这个深度值。 我们自己设置深度值有一个显著缺点,因为只要我们在片段着色器中对`gl_FragDepth`写入什么OpenGL就会关闭所有的前置深度测试。它被关闭的原因是在我们运行片段着色器之前OpenGL搞不清像素的深度值因为片段着色器可能会完全改变这个深度值。
需要考虑到gl_FragDepth写入所带来的性能的下降。然而从OpenGL4.2起我们仍然可以对二者进行一定的调和这需要在片段着色器的顶部使用深度条件depth condition来重新声明gl_FragDepth 因此,你需要考虑到`gl_FragDepth`写入所带来的性能的下降。然而从OpenGL4.2起我们仍然可以对二者进行一定的调和这需要在片段着色器的顶部使用深度条件depth condition来重新声明`gl_FragDepth`
```c++ ```c++
layout (depth_<condition>) out float gl_FragDepth; layout (depth_<condition>) out float gl_FragDepth;
@@ -161,11 +168,11 @@ void main()
### Interface blocks(接口块) ## 接口块(Interface blocks)
到目前位置,每次我们打算从顶点向片段着色器发送数据,我们都会声明一个相互匹配的输/输入变量。从一个着色器向另一个着色器发送数据,一次将它们声明好是最简单的方式,但是随着应用变得越来越大,你也许会打算发送的不仅仅是变量,最好还可以包括数组和结构体。 到目前位置,每次我们打算从顶点向片段着色器发送数据,我们都会声明一个相互匹配的输/输入变量。从一个着色器向另一个着色器发送数据,一次将它们声明好是最简单的方式,但是随着应用变得越来越大,你也许会打算发送的不仅仅是变量,最好还可以包括数组和结构体。
为了帮助我们组织这些变量GLSL为我们提供了一些叫做interface blocks的东西好让我们能够组织这些变量。声明interface block和声明struct有点像不同之处是它现在基于块block使用in和out关键字来声明最后它将成为一个输入或输出块block 为了帮助我们组织这些变量GLSL为我们提供了一些叫做接口块(Interface blocks)的东西,好让我们能够组织这些变量。声明接口块和声明struct有点像不同之处是它现在基于块block使用in和out关键字来声明最后它将成为一个输入或输出块block
```c++ ```c++
#version 330 core #version 330 core
@@ -188,7 +195,7 @@ void main()
} }
``` ```
这次我们声明一个叫做vs_out的interface block,它把我们需要发送给下个阶段着色器的所有输出变量组合起来。虽然这是一个微不足道的例子,但是你可以想象一下,它的确能够帮助我们组织着色器的输入和输出。当我们希望把着色器的输入和输出组织成数组的时候它就变得很有用,我们会在下节几何着色器geometry中见到。 这次我们声明一个叫做vs_out的接口块,它把我们需要发送给下个阶段着色器的所有输出变量组合起来。虽然这是一个微不足道的例子,但是你可以想象一下,它的确能够帮助我们组织着色器的输入和输出。当我们希望把着色器的输入和输出组织成数组的时候它就变得很有用,我们会在下节几何着色器(geometry)中见到。
然后我们还需要在下一个着色器——片段着色器中声明一个输入interface block。块名block name应该是一样的但是实例名可以是任意的。 然后我们还需要在下一个着色器——片段着色器中声明一个输入interface block。块名block name应该是一样的但是实例名可以是任意的。
@@ -211,13 +218,13 @@ void main()
如果两个interface block名一致它们对应的输入和输出就会匹配起来。这是另一个可以帮助我们组织代码的有用功能特别是在跨着色阶段的情况比如几何着色器。 如果两个interface block名一致它们对应的输入和输出就会匹配起来。这是另一个可以帮助我们组织代码的有用功能特别是在跨着色阶段的情况比如几何着色器。
### uniform缓冲对象 ## uniform缓冲对象 (Uniform buffer objects)
我们使用OpenGL很长时间了也学到了一些很酷的技巧但是产生了一些烦恼。比如说当时用一个以上的着色器的时候我们必须一次次设置uniform变量尽管对于每个着色器来说它们都是一样的所以为什么还麻烦地多次设置它们呢 我们使用OpenGL很长时间了也学到了一些很酷的技巧但是产生了一些烦恼。比如说当时用一个以上的着色器的时候我们必须一次次设置uniform变量尽管对于每个着色器来说它们都是一样的所以为什么还麻烦地多次设置它们呢
OpenGL为我们提供了一个叫做uniform缓冲对象的工具使我们能够声明一系列的全局uniform变量 它们会在几个着色器程序中保持一致。当时用uniform缓冲的对象时相关的uniform只能设置一次。我们仍需为每个着色器手工设置唯一的uniform。创建和配置一个uniform缓冲对象需要费点功夫。 OpenGL为我们提供了一个叫做uniform缓冲对象的工具使我们能够声明一系列的全局uniform变量 它们会在几个着色器程序中保持一致。当时用uniform缓冲的对象时相关的uniform只能设置一次。我们仍需为每个着色器手工设置唯一的uniform。创建和配置一个uniform缓冲对象需要费点功夫。
因为uniform缓冲对象是一个缓冲因此我们可以使用glGenBuffers创建一个然后绑定到GL_UNIFORM_BUFFER缓冲目标上然后把所有相关uniform数据存入缓冲。有一些原则像uniform缓冲对象如何储存数据我们会在稍后讨论。首先我们我们在一个简单的顶点着色器中用uniform blockuniform块储存projection和view矩阵: 因为uniform缓冲对象是一个缓冲因此我们可以使用`glGenBuffers`创建一个,然后绑定到`GL_UNIFORM_BUFFER`缓冲目标上然后把所有相关uniform数据存入缓冲。有一些原则像uniform缓冲对象如何储存数据我们会在稍后讨论。首先我们我们在一个简单的顶点着色器中用uniform块(uniform block)储存投影和视图矩阵:
```c++ ```c++
#version 330 core #version 330 core
@@ -239,17 +246,17 @@ void main()
前面大多数例子里我们在每次渲染迭代都为projection和view矩阵设置uniform。这个例子里使用了uniform缓冲对象这非常有用因为这些矩阵我们设置一次就行了。 前面大多数例子里我们在每次渲染迭代都为projection和view矩阵设置uniform。这个例子里使用了uniform缓冲对象这非常有用因为这些矩阵我们设置一次就行了。
在这里我们声明了一个叫做Matrices的uniform block它储存两个4×4矩阵。在uniform block中的变量可以直接获取而不用使用block名作为前缀。接着我们在缓冲中储存这些矩阵的值每个声明了这个uniform block的着色器已经能够获取矩阵 在这里我们声明了一个叫做Matrices的uniform它储存两个4×4矩阵。在uniform中的变量可以直接获取而不用使用block名作为前缀。接着我们在缓冲中储存这些矩阵的值每个声明了这个uniform的着色器能够获取矩阵。
现在你可能会奇怪layout(std140)是什么意思。它的意思是说当前定义的uniform block为它的内容使用特定的内存布局这个声明实际上是设置uniform block layoutuniform块布局 现在你可能会奇怪layout(std140)是什么意思。它的意思是说当前定义的uniform为它的内容使用特定的内存布局这个声明实际上是设置uniform块布局(uniform block layout)
#### uniform block layoutuniform块布局 ### uniform块布局(uniform block layout)
一个uniform block的内容被储存到一个缓冲对象中实际上就是在一块内存中。因为这块内存也不清楚它保存着什么类型的数据我们就必须告诉OpenGL哪一块内存对应着色器中哪一个uniform变量。 一个uniform的内容被储存到一个缓冲对象中实际上就是在一块内存中。因为这块内存也不清楚它保存着什么类型的数据我们就必须告诉OpenGL哪一块内存对应着色器中哪一个uniform变量。
假想下面的uniform block在一个着色器中: 假想下面的uniform在一个着色器中:
```c++ ```c++
layout (std140) uniform ExampleBlock layout (std140) uniform ExampleBlock
@@ -263,37 +270,37 @@ layout (std140) uniform ExampleBlock
}; };
``` ```
我们所希望知道的是每个变量的大小以字节为单位和偏移量从block的起始处所以我们可以以各自的顺序把它们放进一个缓冲里。每个元素的大小在OpenGL中都很清楚直接与C++数据类型呼应向量和矩阵是一个float序列数组。OpenGL没有澄清的是变量之间的间距。这让硬件能以它认为合适的位置方式变量。比如有些硬件可以在float旁边放置一个vec3。不是所有硬件都能这样做在vec3旁边附加一个float之前给vec3加一个边距使之成为4个空间连续的float数组。功能很好但对于我们来说用起来不方便。 我们所希望知道的是每个变量的大小以字节为单位和偏移量从block的起始处所以我们可以以各自的顺序把它们放进一个缓冲里。每个元素的大小在OpenGL中都很清楚直接与C++数据类型呼应向量和矩阵是一个float序列数组。OpenGL没有澄清的是变量之间的间距。这让硬件能以它认为合适的位置方式变量。比如有些硬件可以在float旁边放置一个vec3。不是所有硬件都能这样做在vec3旁边附加一个float之前给vec3加一个边距使之成为4个空间连续的float数组。功能很好但对于我们来说用起来不方便。
GLSL 默认使用的uniform内存布局叫做shared(共享)布局叫共享是因为一旦偏移量被硬件定义它们就会持续地被多个程序所共享。使用共享布局GLSL可以为了优化而重新放置uniform变量只要变量的顺序保持完整。因为我们不知道每个uniform变量的偏移量是多少所以我们也就不知道如何精确地填充uniform缓冲。我们可以使用像glGetUniformIndices这样的函数来查询这个信息但是这超出了本节教程的范围。 GLSL 默认使用的uniform内存布局叫做共享布局(shared layout)叫共享是因为一旦偏移量被硬件定义它们就会持续地被多个程序所共享。使用共享布局GLSL可以为了优化而重新放置uniform变量只要变量的顺序保持完整。因为我们不知道每个uniform变量的偏移量是多少所以我们也就不知道如何精确地填充uniform缓冲。我们可以使用像`glGetUniformIndices`这样的函数来查询这个信息,但是这超出了本节教程的范围。
由于共享布局给我们做了一些空间优化。通常在实践中并不适用分享布局而是使用std140布局。std140通过一系列的规则的规范声明了它们各自的偏移量std140布局为每个变量类型显式地声明了内存的布局。由于被显式的提及我们就可以手工算出每个变量的偏移量。 由于共享布局给我们做了一些空间优化。通常在实践中并不适用分享布局而是使用std140布局。std140通过一系列的规则的规范声明了它们各自的偏移量std140布局为每个变量类型显式地声明了内存的布局。由于被显式的提及我们就可以手工算出每个变量的偏移量。
每个变量都有一个base alignment(基线对齐)它等于在一个uniform block中这个变量所占的空间包含边距这个基线对齐是使用std140布局原则计算出来的。然后我们为每个变量计算出它的aligned offset(对齐偏移)这是一个变量从块block开始的字节偏移量。变量对齐的字节偏移一定等于它的基线对齐的倍数。 每个变量都有一个基线对齐(base alignment)它等于在一个uniform中这个变量所占的空间包含边距这个基线对齐是使用std140布局原则计算出来的。然后我们为每个变量计算出它的对齐偏移(aligned offset)这是一个变量从块block开始的字节偏移量。变量对齐的字节偏移一定等于它的基线对齐的倍数。
准确的布局规则可以在OpenGL的uniform缓冲规范中得到但我们会列出最常见的规范。GLSL中每个变量类型比如int、float和bool被定义为4字节的数量每4字节被表示为N。 准确的布局规则可以[在OpenGL的uniform缓冲规范](http://www.opengl.org/registry/specs/ARB/uniform_buffer_object.txt)中得到但我们会列出最常见的规范。GLSL中每个变量类型比如int、float和bool被定义为4字节每4字节被表示为N。
Type | Layout rule 类型 | 布局规范
---|--- ---|---
Scalar e.g. int or bool | Each scalar has a base alignment of N. 像int和bool这样的标量 | 每个标量的基线为N
Vector | Either 2N or 4N. This means that a vec3 has a base alignment of 4N. 向量 | 每个向量的基线是2N或4N大小。这意味着vec3的基线为4N
Array of scalars or vectors | Each element has a base alignment equal to that of a vec4. 标量与向量数组 | 每个元素的基线与vec4的相同
Matrices | Stored as a large array of column vectors, where each of those vectors has a base alignment of vec4. 矩阵 | 被看做是存储着大量向量的数组,每个元素的基数与vec4相同
Struct | Equal to the computed size of its elements according to the previous rules, but padded to a multiple of the size of a vec4. 结构体 | 根据以上规则计算其各个元素并且间距必须是vec4基线的倍数
像OpenGL大多数规范一样举个例子就很容易理解。再次利用之前介绍的uniform块ExampleBlock我们用std140布局计算它的每个成员的aligned offset对齐偏移 像OpenGL大多数规范一样举个例子就很容易理解。再次利用之前介绍的uniform块`ExampleBlock`我们用std140布局计算它的每个成员的aligned offset对齐偏移
```c++ ```c++
layout (std140) uniform ExampleBlock layout (std140) uniform ExampleBlock
{ {
// // base alignment // aligned offset // base alignment ---------- // aligned offset
float value; // 4 // 0 float value; // 4 // 0
vec3 vector; // 16 // 16 (must be multiple of 16 so 4->16) vec3 vector; // 16 // 16 (必须是16的倍数因此 4->16)
mat4 matrix; // 16 // 32 (column 0) mat4 matrix; // 16 // 32 (第 0 行)
// 16 // 48 (column 1) // 16 // 48 (第 1 行)
// 16 // 64 (column 2) // 16 // 64 (第 2 行)
// 16 // 80 (column 3) // 16 // 80 (第 3 行)
float values[3]; // 16 // 96 (values[0]) float values[3]; // 16 (数组中的标量与vec4相同)//96 (values[0])
// 16 // 112 (values[1]) // 16 // 112 (values[1])
// 16 // 128 (values[2]) // 16 // 128 (values[2])
bool boolean; // 4 // 144 bool boolean; // 4 // 144
@@ -301,33 +308,33 @@ layout (std140) uniform ExampleBlock
}; };
``` ```
尝试自己计算出偏移量把它们和表格对比你可以把这件事当作一个练习。使用计算出来的偏移量根据std140布局规则我们可以用glBufferSubData这样的函数使用变量数据填充缓冲。虽然不是很高效但std140布局可以保证在每个程序中声明的这个uniform块的布局保持一致。 尝试自己计算出偏移量把它们和表格对比你可以把这件事当作一个练习。使用计算出来的偏移量根据std140布局规则我们可以用`glBufferSubData`这样的函数使用变量数据填充缓冲。虽然不是很高效但std140布局可以保证在每个程序中声明的这个uniform块的布局保持一致。
在定义uniform块前面添加layout (std140)声明我们就能告诉OpenGL这个uniform块使用了std140布局。另外还有两种其他的布局可以选择它们需要我们在填充缓冲之前查询每个偏移量。我们已经了解了分享布局shared layout和其他的布局都将被封装packed。当使用封装packed布局的时候不能保证布局在别的程序中能够保持一致因为它允许编译器从uniform block块中优化出去uniform变量这在每个着色器中都可能不同。 在定义uniform块前面添加layout (std140)声明我们就能告诉OpenGL这个uniform块使用了std140布局。另外还有两种其他的布局可以选择它们需要我们在填充缓冲之前查询每个偏移量。我们已经了解了分享布局shared layout和其他的布局都将被封装packed。当使用封装packed布局的时候不能保证布局在别的程序中能够保持一致因为它允许编译器从uniform块中优化出去uniform变量这在每个着色器中都可能不同。
#### 使用uniform缓冲 ### 使用uniform缓冲
我们讨论了uniform block在着色器中的定义和如何定义它们的内存布局,但是我们还没有讨论如何使用它们。 我们讨论了uniform在着色器中的定义和如何定义它们的内存布局,但是我们还没有讨论如何使用它们。
首先我们需要创建一个uniform缓冲对象这要使用glGenBuffers来完成。当我们拥有了一个缓冲对象我们就把它绑定到GL_UNIFORM_BUFFER目标上调用glBufferData来给它分配足够的空间。 首先我们需要创建一个uniform缓冲对象这要使用`glGenBuffers`来完成。当我们拥有了一个缓冲对象,我们就把它绑定到`GL_UNIFORM_BUFFER`目标上,调用`glBufferData`来给它分配足够的空间。
```c++ ```c++
GLuint uboExampleBlock; GLuint uboExampleBlock;
glGenBuffers(1, &uboExampleBlock); glGenBuffers(1, &uboExampleBlock);
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock); glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
glBufferData(GL_UNIFORM_BUFFER, 150, NULL, GL_STATIC_DRAW); // allocate 150 bytes of memory glBufferData(GL_UNIFORM_BUFFER, 150, NULL, GL_STATIC_DRAW); // 分配150个字节的内存空间
glBindBuffer(GL_UNIFORM_BUFFER, 0); glBindBuffer(GL_UNIFORM_BUFFER, 0);
``` ```
现在任何时候当我们打算往缓冲中更新或插入数据我们就绑定到uboExampleBlock上并使用glBufferSubData来更新它的内存。我们只需要更新这个uniform缓冲一次所有的使用这个缓冲着色器就都会使用它更新的数据了。但是OpenGL是如何知道哪个uniform缓冲对应哪个uniform block呢? 现在任何时候当我们打算往缓冲中更新或插入数据,我们就绑定到`uboExampleBlock`上,并使用`glBufferSubData`来更新它的内存。我们只需要更新这个uniform缓冲一次所有的使用这个缓冲着色器就都会使用它更新的数据了。但是OpenGL是如何知道哪个uniform缓冲对应哪个uniform呢?
在OpenGL环境context定义了若干绑定点binding points在哪儿我们可以把一个uniform缓冲链接上去。当我们创建了一个uniform缓冲我们把它链接到一个这个绑定点上我们也把着色器中uniform block链接到同一个绑定点上,这样就把它们链接到一起了。下面的图标表示了这点: 在OpenGL环境context定义了若干绑定点binding points在哪儿我们可以把一个uniform缓冲链接上去。当我们创建了一个uniform缓冲我们把它链接到一个这个绑定点上我们也把着色器中uniform链接到同一个绑定点上,这样就把它们链接到一起了。下面的图标表示了这点:
![](http://bullteacher.com/wp-content/uploads/2015/06/advanced_glsl_binding_points.png) ![](http://learnopengl.com/img/advanced/advanced_glsl_binding_points.png)
你可以看到我们可以将多个uniform缓冲绑定不同绑定点上。因为着色器A和着色器B都有一个链接到同一个绑定点0的uniform block它们的uniform block分享同样的uniform数据—uboMatrices有一个前提条件是两个着色器必须都定义了Matrices这个uniform block 你可以看到我们可以将多个uniform缓冲绑定不同绑定点上。因为着色器A和着色器B都有一个链接到同一个绑定点0的uniform它们的uniform分享同样的uniform数据—`uboMatrices`有一个前提条件是两个着色器必须都定义了Matrices这个uniform
我们调用glUniformBlockBinding函数来把uniform block设置到一个特定的绑定点上。函数的第一个参数是一个程序对象接着是一个uniform block索引uniform block index和打算链接的绑定点。uniform block索引是一个着色器中定义的uniform block的索引位置可以调用glGetUniformBlockIndex来获取这个值这个函数接收一个程序对象和uniform block的名字。我们可以从图表设置Lights这个uniform block链接到绑定点2 我们调用`glUniformBlockBinding`函数来把uniform设置到一个特定的绑定点上。函数的第一个参数是一个程序对象接着是一个uniform索引uniform block index和打算链接的绑定点。uniform索引是一个着色器中定义的uniform的索引位置,可以调用`glGetUniformBlockIndex`来获取这个值这个函数接收一个程序对象和uniform的名字。我们可以从图表设置Lights这个uniform链接到绑定点2
```c++ ```c++
GLuint lights_index = glGetUniformBlockIndex(shaderA.Program, "Lights"); GLuint lights_index = glGetUniformBlockIndex(shaderA.Program, "Lights");
@@ -336,40 +343,39 @@ glUniformBlockBinding(shaderA.Program, lights_index, 2);
注意,我们必须在每个着色器中重复做这件事。 注意,我们必须在每个着色器中重复做这件事。
从OpenGL4.2起也可以在着色器中通过添加另一个布局标识符来储存一个uniform block的绑定点就不用我们调用glGetUniformBlockIndexglUniformBlockBinding了。下面的代表显式设置了Lights这个uniform Block的绑定点: 从OpenGL4.2起也可以在着色器中通过添加另一个布局标识符来储存一个uniform的绑定点,就不用我们调用`glGetUniformBlockIndex`和`glUniformBlockBinding`了。下面的代表显式设置了Lights这个uniform的绑定点:
```c++ ```c++
layout(std140, binding = 2) uniform Lights { ... }; layout(std140, binding = 2) uniform Lights { ... };
``` ```
然后我们还需要把uniform缓冲对象绑定到同样的绑定点上这个可以使用glBindBufferBaseglBindBufferRange来完成。 然后我们还需要把uniform缓冲对象绑定到同样的绑定点上这个可以使用`glBindBufferBase`或`glBindBufferRange`来完成。
```c++ ```c++
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock); glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock);
// or // 或者
glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 150); glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 150);
``` ```
函数glBindBufferBase接收一个目标、一个绑定点索引和一个uniform缓冲对象作为它的参数。这个函数把uboExampleBlock链接到绑定点2上面自此绑定点所链接的两端都链接在一起了。你还可以使用glBindBufferRange函数这个函数还需要一个偏移量和大小作为参数这样你就可以只把一定范围的uniform缓冲绑定到一个绑定点上了。使用glBindBufferRage函数你能够将多个不同的uniform block链接到同一个uniform缓冲对象上。 函数`glBindBufferBase`接收一个目标、一个绑定点索引和一个uniform缓冲对象作为它的参数。这个函数把`uboExampleBlock`链接到绑定点2上面自此绑定点所链接的两端都链接在一起了。你还可以使用`glBindBufferRange`函数这个函数还需要一个偏移量和大小作为参数这样你就可以只把一定范围的uniform缓冲绑定到一个绑定点上了。使用`glBindBufferRage`函数你能够将多个不同的uniform链接到同一个uniform缓冲对象上。
现在所有事情都做好了我们可以开始向uniform缓冲添加数据了。我们可以使用glBufferSubData将所有数据添加为一个单独的字节数组或者更新缓冲的部分内容只要我们愿意。为了更新uniform变量boolean我们可以这样更新uniform缓冲对象 现在所有事情都做好了我们可以开始向uniform缓冲添加数据了。我们可以使用`glBufferSubData`将所有数据添加为一个单独的字节数组或者更新缓冲的部分内容只要我们愿意。为了更新uniform变量boolean我们可以这样更新uniform缓冲对象
```c++ ```c++
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock); glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
GLint b = true; // bools in GLSL are represented as 4 bytes, so we store it in an integer GLint b = true; // GLSL中的布尔值是4个字节因此我们将它创建为一个4字节的整数
glBufferSubData(GL_UNIFORM_BUFFER, 142, 4, &b); glBufferSubData(GL_UNIFORM_BUFFER, 142, 4, &b);
glBindBuffer(GL_UNIFORM_BUFFER, 0); glBindBuffer(GL_UNIFORM_BUFFER, 0);
``` ```
同样的处理也能够应用到uniform block中其他uniform变量上。 同样的处理也能够应用到uniform中其他uniform变量上。
#### 一个简单的例子 ### 一个简单的例子
我们来师范一个真实的使用uniform缓冲对象的例子。如果我们回头看看前面所有演示的代码我们一直使用了3个矩阵投影、视图和模型矩阵。所有这些矩阵中只有模型矩阵是频繁变化的。如果我们有多个着色器使用了这些矩阵我们可能最好还是使用uniform缓冲对象。 我们来师范一个真实的使用uniform缓冲对象的例子。如果我们回头看看前面所有演示的代码我们一直使用了3个矩阵投影、视图和模型矩阵。所有这些矩阵中只有模型矩阵是频繁变化的。如果我们有多个着色器使用了这些矩阵我们可能最好还是使用uniform缓冲对象。
我们将把投影和视图矩阵储存到一个uniform block中它被取名为Matrices。我们不打算储存模型矩阵因为模型矩阵会频繁在着色器间更改所以使用uniform缓冲对象真的不会带来什么好处。
我们将把投影和视图矩阵储存到一个uniform块中它被取名为Matrices。我们不打算储存模型矩阵因为模型矩阵会频繁在着色器间更改所以使用uniform缓冲对象真的不会带来什么好处。
```c++ ```c++
#version 330 core #version 330 core
@@ -388,9 +394,9 @@ void main()
} }
``` ```
这儿没什么特别的除了我们现在使用了一个带有std140布局的uniform block。我们在例程中将显示4个立方体每个立方体都使用一个不同的着色器程序。4个着色器程序使用同样的顶点着色器但是它们将使用各自的片段着色器每个片段着色器输出一个单色。 这儿没什么特别的除了我们现在使用了一个带有std140布局的uniform。我们在例程中将显示4个立方体每个立方体都使用一个不同的着色器程序。4个着色器程序使用同样的顶点着色器但是它们将使用各自的片段着色器每个片段着色器输出一个单色。
首先我们把顶点着色器的uniform block设置为绑定点0。注意我们必须为每个着色器做这件事。 首先我们把顶点着色器的uniform设置为绑定点0。注意我们必须为每个着色器做这件事。
```c++ ```c++
GLuint uniformBlockIndexRed = glGetUniformBlockIndex(shaderRed.Program, "Matrices"); GLuint uniformBlockIndexRed = glGetUniformBlockIndex(shaderRed.Program, "Matrices");
@@ -417,9 +423,9 @@ glBindBuffer(GL_UNIFORM_BUFFER, 0);
glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4)); glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4));
``` ```
我们纤维我们的缓冲分配足够的内存它等于glm::mat4的2倍。GLM的矩阵类型的大小直接对应于GLSL的mat4。然后我们把一个特定范围的缓冲链接到绑定点0这个例子中应该是整个缓冲。 我们先为缓冲分配足够的内存它等于glm::mat4的2倍。GLM的矩阵类型的大小直接对应于GLSL的mat4。然后我们把一个特定范围的缓冲链接到绑定点0这个例子中应该是整个缓冲。
现在所有要做的事只剩下填充缓冲了。如果我们把视野( field of view值保持为恒定的投影矩阵这样就不会有摄像机缩放我们只要在程序中定义它一次就行了这也意味着我们只需向缓冲中把它插入一次。因为我们已经在缓冲对象中分配了足够的内存我们可以在我们进入游戏循环之前使用glBufferSubData来储存投影矩阵 现在所有要做的事只剩下填充缓冲了。如果我们把视野( field of view值保持为恒定的投影矩阵这样就不会有摄像机缩放我们只要在程序中定义它一次就行了这也意味着我们只需向缓冲中把它插入一次。因为我们已经在缓冲对象中分配了足够的内存我们可以在我们进入游戏循环之前使用`glBufferSubData`来储存投影矩阵:
```c++ ```c++
glm::mat4 projection = glm::perspective(45.0f, (float)width/(float)height, 0.1f, 100.0f); glm::mat4 projection = glm::perspective(45.0f, (float)width/(float)height, 0.1f, 100.0f);
@@ -438,27 +444,27 @@ glBufferSubData(
glBindBuffer(GL_UNIFORM_BUFFER, 0); glBindBuffer(GL_UNIFORM_BUFFER, 0);
``` ```
这就是uniform缓冲对象。每个包含着Matrices这个uniform block的顶点着色器豆浆邦汉uboMatrices所储存的数据。所以如果我们现在使用4个不同的着色器绘制4个立方体它们的投影和视图矩阵都是一样的 这就是uniform缓冲对象。每个包含着`Matrices`这个uniform块的顶点着色器都将对应`uboMatrices`所储存的数据。所以如果我们现在使用4个不同的着色器绘制4个立方体它们的投影和视图矩阵都是一样的
```c++ ```c++
glBindVertexArray(cubeVAO); glBindVertexArray(cubeVAO);
shaderRed.Use(); shaderRed.Use();
glm::mat4 model; glm::mat4 model;
model = glm::translate(model, glm::vec3(-0.75f, 0.75f, 0.0f)); // Move top-left model = glm::translate(model, glm::vec3(-0.75f, 0.75f, 0.0f)); // 移动到左上方
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, 0, 36); glDrawArrays(GL_TRIANGLES, 0, 36);
// ... Draw Green Cube // ... 绘制绿色立方体
// ... Draw Blue Cube // ... 绘制蓝色立方体
// ... Draw Yellow Cube // ... 绘制黄色立方体
glBindVertexArray(0); glBindVertexArray(0);
``` ```
我们只需要在去设置一个model的unform即可。在一个像这样的场景中使用uniform缓冲对象在每个着色器中可以减少uniform的调用。最后效果看起来像这样 我们只需要在去设置一个`model`的uniform即可。在一个像这样的场景中使用uniform缓冲对象在每个着色器中可以减少uniform的调用。最后效果看起来像这样
![](http://learnopengl.com/img/advanced/advanced_glsl_uniform_buffer_objects.png) ![](http://learnopengl.com/img/advanced/advanced_glsl_uniform_buffer_objects.png)
通过改变模型矩阵每个立方体都移动到窗口的一边由于片段着色器不同物体的颜色也不同。这是一个相对简单的场景我们可以使用uniform缓冲对象但是任何大型渲染程序有成百上千的活动着色程序彼时uniform缓冲对象就会闪闪发光了。 通过改变模型矩阵每个立方体都移动到窗口的一边由于片段着色器不同物体的颜色也不同。这是一个相对简单的场景我们可以使用uniform缓冲对象但是任何大型渲染程序有成百上千的活动着色程序彼时uniform缓冲对象就会闪闪发光了。
你可以[在这里获得例程的完整源码](http://www.learnopengl.com/code_viewer.php?code=advanced/advanced_glsl_uniform_buffer_objects)。 你可以[在这里获得例程的完整源码](http://www.learnopengl.com/code_viewer.php?code=advanced/advanced_glsl_uniform_buffer_objects)。
uniform缓冲对象比单独的uniform有很多好处。第一一次设置多个uniform比一次设置一个速度快。第二如果你打算改变一个横跨多个着色器的uniform在uniform缓冲中只需更改一次。最后一个好处可能不是很明显使用uniform缓冲对象你可以在着色器中使用更多的uniform。OpenGL有一个对可使用uniform数据的数量的限制可以用GL_MAX_VERTEX_UNIFORM_COMPONENTS来获取。当使用uniform缓冲对象中这个限制的阈限会更高。所以无论何时你达到了uniform的最大使用数量比如做谷歌动画的时候你可以使用uniform缓冲对象。 uniform缓冲对象比单独的uniform有很多好处。第一一次设置多个uniform比一次设置一个速度快。第二如果你打算改变一个横跨多个着色器的uniform在uniform缓冲中只需更改一次。最后一个好处可能不是很明显使用uniform缓冲对象你可以在着色器中使用更多的uniform。OpenGL有一个对可使用uniform数据的数量的限制可以用`GL_MAX_VERTEX_UNIFORM_COMPONENTS`来获取。当使用uniform缓冲对象中这个限制的阈限会更高。所以无论何时你达到了uniform的最大使用数量比如做骨骼动画的时候你可以使用uniform缓冲对象。