diff --git a/04 Advanced OpenGL/05 Framebuffers.md b/04 Advanced OpenGL/05 Framebuffers.md index 68b82ed..92ca7e7 100644 --- a/04 Advanced OpenGL/05 Framebuffers.md +++ b/04 Advanced OpenGL/05 Framebuffers.md @@ -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为我们做了这件事)。通过创建我们自己的帧缓冲我们能够获得一种额外的渲染方式。 -你不会立刻理解应用程序的帧缓的含义,但是把你的场景渲染到一个不同的帧缓冲中,可以使我们能够在场景中创建镜子这样的效果,或者做出一些炫酷的后处理特效。首先我们会讨论它们是如何工作的,然后我们将利用它们来实现一些炫酷的后处理效果。 +你也许不能立刻理解应用程序的帧缓冲的含义,通过帧缓冲可以将你的场景渲染到一个不同的帧缓冲中,可以使我们能够在场景中创建镜子这样的效果,或者做出一些炫酷的特效。首先我们会讨论它们是如何工作的,然后我们将利用帧缓冲来实现一些炫酷的效果。 +## 创建一个帧缓冲 - -###创建一个帧缓冲 - -就像OpenGL中其他对象一样,我们可以使用一个叫做glGenFramebuffers的函数来创建一个帧缓冲对象(简称FBO): +就像OpenGL中其他对象一样,我们可以使用一个叫做`glGenFramebuffers`的函数来创建一个帧缓冲对象(简称FBO): ```c++ GLuint fbo; glGenFramebuffers(1, &fbo); ``` -这种对象的创建和使用的方式我们已经见过不少了,因此它们的使用方式也和之前我们见过的其他对象的使用方式相似;首先我们要创建一个帧缓冲对象,把它绑定到当前帧缓冲,做一些操作,然后解绑帧缓冲。我们使用glBindFramebuffer来绑定帧缓冲: +这种对象的创建和使用的方式我们已经见过不少了,因此它们的使用方式也和之前我们见过的其他对象的使用方式相似。首先我们要创建一个帧缓冲对象,把它绑定到当前帧缓冲,做一些操作,然后解绑帧缓冲。我们使用`glBindFramebuffer`来绑定帧缓冲: ```c++ glBindFramebuffer(GL_FRAMEBUFFER, fbo); ``` -绑定到GL_FRAMEBUFFER目标后,接下来所有的读、写帧缓冲的操作都会影响到当前绑定的帧缓冲。也可以把帧缓冲分开绑定到读或写目标上,分别使用GL_READ_FRAMEBUFFER或GL_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++ if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) // Execute victory dance - ``` +``` -后续所有渲染操作将渲染到当前绑定的帧缓冲的附加缓冲中,由于我们的帧缓冲不是默认的帧缓冲,渲染命令对窗口的视频输出不会产生任何影响。出于这个原因,它被称为离屏渲染(off-screen rendering),就是渲染到一个另外的缓冲中。为了让所有的渲染操作对主窗口产生影响我们必须通过绑定为0来使默认帧缓冲激活: +后续所有渲染操作将渲染到当前绑定的帧缓冲的附加缓冲中,由于我们的帧缓冲不是默认的帧缓冲,渲染命令对窗口的视频输出不会产生任何影响。出于这个原因,它被称为离屏渲染(off-screen rendering),就是渲染到一个另外的缓冲中。为了让所有的渲染操作对主窗口产生影响我们必须通过绑定为0来使默认帧缓冲被激活: ```c++ glBindFramebuffer(GL_FRAMEBUFFER, 0); @@ -57,7 +61,7 @@ glDeleteFramebuffers(1, &fbo); 现在在执行完成检测前,我们需要把一个或更多的附件附加到帧缓冲上。一个附件就是一个内存地址,这个内存地址里面包含一个为帧缓冲准备的缓冲,它可以是个图像。当创建一个附件的时候我们有两种方式可以采用:纹理或渲染缓冲(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); ``` -这里主要的区别是我们把维度设置为屏幕代销(尽管不是必须的),我们还传递NULL作为纹理的data参数。对于这个纹理,我们之分配内存,而不去填充它。纹理填充会在渲染到帧缓冲的时候去做。同样,要注意,我们不关心放置方式或者mipmap的事,因为在大多数时候都不会需要它们的。 +这里主要的区别是我们把纹理的维度设置为屏幕大小(尽管不是必须的),我们还传递NULL作为纹理的data参数。对于这个纹理,我们只分配内存,而不去填充它。纹理填充会在渲染到帧缓冲的时候去做。同样,要注意,我们不用关心环绕方式或者Mipmap,因为在大多数时候都不会需要它们的。 + +如果你打算把整个屏幕渲染到一个或大或小的纹理上,你需要用新的纹理的尺寸作为参数再次调用`glViewport`(要在渲染到你的帧缓冲之前做好),否则只有一小部分纹理或屏幕能够绘制到纹理上。 -如果你打算把整个屏幕渲染到一个或大或小的纹理上,你需要用新的纹理的尺寸作为参数再次调用glViewport(要在渲染到你的帧缓冲之前做好),否则只有一小部分纹理或屏幕能够绘制到纹理上。 现在我们已经创建了一个纹理,最后一件要做的事情是把它附加到帧缓冲上: ```c++ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D, texture, 0); ``` -glFramebufferTexture2D函数有以下参数: + +`glFramebufferTexture2D`函数需要传入下列参数: * target:我们所创建的帧缓冲类型的目标(绘制、读取或两者都有)。 * attachment:我们所附加的附件的类型。现在我们附加的是一个颜色附件。需要注意,最后的那个0是暗示我们可以附加1个以上颜色的附件。我们会在后面的教程中谈到。 * textarget:你希望附加的纹理类型。 * texture:附加的实际纹理。 -* level:mipmap level。我们设置为0. +* level:Mipmap 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++ -glTexImage2D( - GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 800, 600, 0, - GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL -); +glTexImage2D( 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); ``` -渲染缓冲对象附件(Renderbuffer object attachments) + +### 渲染缓冲对象附件(Renderbuffer object attachments) 在介绍了帧缓冲的可行附件类型——纹理后,OpenGL引进了渲染缓冲对象(Renderbuffer objects),所以在过去那些美好时光里纹理是附件的唯一可用的类型。和纹理图像一样,渲染缓冲对象也是一个缓冲,它可以是一堆字节、整数、像素或者其他东西。渲染缓冲对象的一大优点是,它以OpenGL原生渲染格式储存它的数据,因此在离屏渲染到帧缓冲的时候,这些数据就相当于被优化过的了。 -渲染缓冲对象将所有渲染数据直接储存到它们的缓冲里,而不会进行针对特定纹理格式的任何转换,这样它们就成了一种快速可写的存储介质了。然而,渲染缓冲对象通常是只写的,要不能读取它们(就像获取纹理,不能写入纹理一样)。可以用glReadPixels函数去读取,函数返回一个当前绑定的帧缓冲的特定像素区域,而不是直接返回附件本身。 +渲染缓冲对象将所有渲染数据直接储存到它们的缓冲里,而不会进行针对特定纹理格式的任何转换,这样它们就成了一种快速可写的存储介质了。然而,渲染缓冲对象通常是只写的,不能修改它们(就像获取纹理,不能写入纹理一样)。可以用`glReadPixels`函数去读取,函数返回一个当前绑定的帧缓冲的特定像素区域,而不是直接返回附件本身。 -因为它们的数据已经是原生格式了,在写入或把它们的数据简单地到其他缓冲的时候非常快。当使用渲染缓冲对象时,像切换缓冲这种操作变得异常高速。我们在没个渲染迭代末尾使用的那个glfwSwapBuffers函数,同样以渲染缓冲对象实现:我们简单地写入到一个渲染缓冲图像,最后交换到另一个里。渲染缓冲对象对于这种操作来说很完美。 +因为它们的数据已经是原生格式了,在写入或把它们的数据简单地到其他缓冲的时候非常快。当使用渲染缓冲对象时,像切换缓冲这种操作变得异常高速。我们在每个渲染迭代末尾使用的那个`glfwSwapBuffers`函数,同样以渲染缓冲对象实现:我们简单地写入到一个渲染缓冲图像,最后交换到另一个里。渲染缓冲对象对于这种操作来说很完美。 创建一个渲染缓冲对象和创建帧缓冲代码差不多: @@ -117,21 +121,21 @@ GLuint rbo; glGenRenderbuffers(1, &rbo); ``` -相似地,我们打算把渲染缓冲对象绑定,这样所有后续渲染缓冲操作都会影响到当前的RBO: +相似地,我们打算把渲染缓冲对象绑定,这样所有后续渲染缓冲操作都会影响到当前的渲染缓冲对象: ```c++ glBindRenderbuffer(GL_RENDERBUFFER, rbo); ``` -由于渲染缓冲对象通常是只写的,它们经常作为深度和模板附件来使用,由于大多数时候,我们不需要从深度和模板缓冲中读取数据,但仍关系深度和模板测试。我们就需要有深度和模板值提供给测试,但不需要对这些值进行采样(sample),所以深度缓冲对象是完全符合的。当我们不去从这些缓冲中采样的时候,渲染缓冲对象通常很合适,因为它们等于是优化过的。 +由于渲染缓冲对象通常是只写的,它们经常作为深度和模板附件来使用,由于大多数时候,我们不需要从深度和模板缓冲中读取数据,但仍关心深度和模板测试。我们就需要有深度和模板值提供给测试,但不需要对这些值进行采样(sample),所以深度缓冲对象是完全符合的。当我们不去从这些缓冲中采样的时候,渲染缓冲对象通常很合适,因为它们等于是被优化过的。 -调用glRenderbufferStorage函数可以创建一个深度和模板渲染缓冲对象: +调用`glRenderbufferStorage`函数可以创建一个深度和模板渲染缓冲对象: ```c 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确定可以进行深度测试(模板测试,如果你用的话)所以我们必须还要确保向帧缓冲中添加一个深度(和模板)附件。由于我们只采样颜色缓冲,并不采样其他缓冲,我们可以创建一个渲染缓冲对象来达到这个目的。记住,当你不打算从指定缓冲采样的的时候,它们是一个不错的选择。 -创建一个渲染缓冲对象不太难。唯一一件要记住的事情是,我们正在创建的是一个渲染缓冲对象的深度和模板附件。我们把它的内部给事设置为GL_DEPTH24_STENCIL8,对于我们的目的来说这个精确度已经足够了。 +创建一个渲染缓冲对象不太难。唯一一件要记住的事情是,我们正在创建的是一个渲染缓冲对象的深度和模板附件。我们把它的内部给事设置为`GL_DEPTH24_STENCIL8`,对于我们的目的来说这个精确度已经足够了。 ```c++ GLuint rbo; @@ -185,7 +189,7 @@ glBindRenderbuffer(GL_RENDERBUFFER, 0); 接着,在做好帧缓冲之前,还有最后一步,我们把渲染缓冲对象附加到帧缓冲的深度和模板附件上: -```c +```c++ 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++ #version 330 core @@ -262,7 +267,7 @@ glDrawArrays(GL_TRIANGLES, 0, 6); 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减去它: @@ -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 在单独纹理图像上进行后处理的另一个好处是我们可以从纹理的其他部分进行采样。比如我们可以从当前纹理值的周围采样多个纹理值。创造性地把它们结合起来就能创造出有趣的效果了。 @@ -333,7 +338,7 @@ kernel是一个长得有点像一个小矩阵的数值数组,它中间的值 !!! Important - 你在网上能找到的kernel的例子大多数都是所有值加起来等于1,如果加起来不等于1就意味着这个纹理值比原来更大或者更小了。 + 你在网上能找到的kernel的例子大多数都是所有值加起来等于1,如果加起来不等于1就意味着这个纹理值比原来更大或者更小了。 kernel对于后处理来说非常管用,因为用起来简单。网上能找到有很多实例,为了能用上kernel我们还得改改片段着色器。这里假设每个kernel都是3×3(实际上大多数都是3×3): @@ -381,11 +386,11 @@ void main() 这里创建的有趣的效果就好像你的玩家吞了某种麻醉剂产生的幻觉一样。 -###Blur +### Blur 创建模糊效果的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数组会是这样的: @@ -405,7 +410,7 @@ float kernel[9] = float[]( 你可以看到我们一旦拥有了这个kernel的实现以后,创建一个后处理特效就不再是一件难事.最后,我们再来讨论一个流行的特效,以结束本节内容. -###边检测 +### 边检测 下面的边检测kernel与锐化kernel类似: @@ -415,8 +420,9 @@ float kernel[9] = float[]( ![](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. diff --git a/04 Advanced OpenGL/06 Cubemaps.md b/04 Advanced OpenGL/06 Cubemaps.md index bd7a116..270f9d6 100644 --- a/04 Advanced OpenGL/06 Cubemaps.md +++ b/04 Advanced OpenGL/06 Cubemaps.md @@ -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的单位立方体,有个以原点为起点的方向向量在它的中心。 @@ -12,14 +16,14 @@ !!! 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++ GLuint textureID; @@ -27,11 +31,11 @@ glGenTextures(1, &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_NEGATIVE_X | 左 @@ -40,10 +44,9 @@ GL_TEXTURE_CUBE_MAP_NEGATIVE_Y | 下 GL_TEXTURE_CUBE_MAP_POSITIVE_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++ - int width,height; unsigned char* image; 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 ); } - ``` -这儿我们有个vector叫textures_faces,它包含cubemap所需所有纹理位置,并且以上表所列的顺序给出。它为每个当前绑定的cubemp的每个面生成一个纹理。 +这儿我们有个vector叫`textures_faces`,它包含cubemap所各个纹理的文件路径,并且以上表所列的顺序排列。它将为每个当前绑定的cubemp的每个面生成一个纹理。 -由于cubemap和其他纹理没什么不同,我们也要定义它的放置方式和过滤方式: +由于cubemap和其他纹理没什么不同,我们也要定义它的环绕方式和过滤方式: ```c++ - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - ``` -在片段着色器中,我们也必须使用一个不同的采样器——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++ +in vec3 textureDir; // 用一个三维方向向量来表示Cubemap纹理的坐标 -in vec3 textureDir; // Direction vector representing a 3D texture coordinate -uniform samplerCube cubemap; // Cubemap texture sampler +uniform samplerCube cubemap; // Cubemap纹理采样器 void main() { @@ -84,33 +88,29 @@ void main() } ``` -看起来不错,但是何必这么做呢?因为恰巧使用cubemap可以简单的实现很多有意思的技术。其中之一便是天空盒(skybox)。 +看起来不错,但是何必这么做呢?因为恰巧使用cubemap可以简单的实现很多有意思的技术。其中之一便是著名的**天空盒(Skybox)**。 -### 天空盒(Skybox) +## 天空盒(Skybox) -天空盒是一个包裹整个场景的立方体,它由6个图像构成一个环绕的环境,给玩家一种他所在的场景比实际的要大得多的幻觉。比如有些在视频游戏中使用的天空盒的图像是群山、白云或者满天繁星。比如下面的夜空繁星的图像就来自《上古卷轴》3: - -别被GL_TEXTURE_WRAP_R吓到,它只是简单的设置了纹理的R坐标,R坐标对应于纹理的第三个维度(就像位置的z一样)。我们把放置方式设置为GL_CLAMP_TO_EDGE,由于纹理坐标在两个面之间,所以可能并不能触及哪个面(由于硬件限制),因此使用GL_CLAMP_TO_EDGE后OpenGL会返回它们的边界的值,尽管我们可能在两个两个面中间进行的采样。 - -在绘制物体之前,将使用cubemap,而在渲染前我们要激活相应的纹理单元并绑定到cubemap上,这和普通的2D纹理没什么区别。 +天空盒是一个包裹整个场景的立方体,它由6个图像构成一个环绕的环境,给玩家一种他所在的场景比实际的要大得多的幻觉。比如有些在视频游戏中使用的天空盒的图像是群山、白云或者满天繁星。比如下面的夜空繁星的图像就来自《上古卷轴》: ![](http://learnopengl.com/img/advanced/cubemaps_morrowind.jpg) 你现在可能已经猜到cubemap完全满足天空盒的要求:我们有一个立方体,它有6个面,每个面需要一个贴图。上图中使用了几个夜空的图片给予玩家一种置身广袤宇宙的感觉,可实际上,他还是在一个小盒子之中。 -网上有很多这样的天空盒的资源。这个网站就提供了很多。这些天空盒图像通常有下面的样式: +网上有很多这样的天空盒的资源。[这个网站](http://www.custommapmakers.org/skyboxes.php)就提供了很多。这些天空盒图像通常有下面的样式: ![](http://learnopengl.com/img/advanced/cubemaps_skybox.png) 如果你把这6个面折叠到一个立方体中,你机会获得模拟了一个巨大的风景的立方体。有些资源所提供的天空盒比如这个例子6个图是连在一起的,你必须手工它们切割出来,不过大多数情况它们都是6个单独的纹理图像。 -这个细致(高精度)的天空盒就是我们将在场景中使用的那个,你可以在这里下载。 +这个细致(高精度)的天空盒就是我们将在场景中使用的那个,你可以[在这里下载](http://learnopengl.com/img/textures/skybox.rar)。 -#### 加载一个天空盒 +### 加载一个天空盒 -由于天空盒实际上就是一个cubemap,加载天空盒和之前我们看到的没什么大的不同。为了加载天空盒我们将使用下面的函数,它接收一个包含6个纹理位置的vector: +由于天空盒实际上就是一个cubemap,加载天空盒和之前我们加载cubemap的没什么大的不同。为了加载天空盒我们将使用下面的函数,它接收一个包含6个纹理文件路径的vector: ```c++ GLuint loadCubemap(vector faces) @@ -157,13 +157,13 @@ faces.push_back("front.jpg"); 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立方体帖上纹理,可以用立方体的位置作为纹理坐标进行采样。当一个立方体的中心位于原点(0,0,0)的时候,它的每一个位置向量也就是以原点为起点的方向向量。这个方向向量就是我们要得到的立方体某个位置的相应纹理值。出于这个理由,我们只需要提供位置向量,而无需纹理坐标。为了渲染天空盒,我们需要一组新着色器,它们不会太复杂。因为我们只有一个顶点属性,顶点着色器非常简单: @@ -197,7 +197,7 @@ void main() } ``` -片段着色器比较明了。我们把顶点属性中的位置向量作为纹理的方向向量,使用它们从cubemap采样纹理值。渲染天空盒现在很简单,我们有了一个cubemap纹理,我们简单绑定cubemap纹理,天空盒就自动地用天空盒的cubemap填充了。为了绘制天空盒,我们将把它作为场景中第一个绘制的物体并且关闭深度写入。这样天空盒才能成为所有其他物体的背景来绘制出来。 +片段着色器比较明了,我们把顶点属性中的位置向量作为纹理的方向向量,使用它们从cubemap采样纹理值。渲染天空盒现在很简单,我们有了一个cubemap纹理,我们简单绑定cubemap纹理,天空盒就自动地用天空盒的cubemap填充了。为了绘制天空盒,我们将把它作为场景中第一个绘制的物体并且关闭深度写入。这样天空盒才能成为所有其他物体的背景来绘制出来。 ```c++ @@ -226,13 +226,13 @@ glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix())); 尝试用不同的天空盒实验,看看它们对场景有多大影响。 -#### 优化 +### 优化 现在我们在渲染场景中的其他物体之前渲染了天空盒。这么做没错,但是不怎么高效。如果我们先渲染了天空盒,那么我们就是在为每一个屏幕上的像素运行片段着色器,即使天空盒只有部分在显示着;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++ void main() @@ -245,19 +245,19 @@ void main() 最终,标准化设备坐标就总会有个与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) @@ -282,7 +282,7 @@ void main() } ``` -我们先来计算观察/摄像机方向向量I,然后使用它来计算反射向量R,接着我们用R从天空盒cubemap采样。要注意的是,我们有了fragment的插值Normal和Position变量,所以我们需要修正顶点着色器适应它。 +我们先来计算观察/摄像机方向向量I,然后使用它来计算反射向量R,接着我们用R从天空盒cubemap采样。要注意的是,我们有了片段的插值Normal和Position变量,所以我们需要修正顶点着色器适应它。 ```c++ #version 330 core @@ -304,9 +304,9 @@ void main() } ``` -我们用了法线向量,所以我们打算使用一个法线矩阵(normal matrix)变换它们。Position输出的向量是一个世界空间位置向量。顶点着色器输出的Position用来在片段着色器计算观察方向向量。 +我们用了法线向量,所以我们打算使用一个**法线矩阵(normal matrix)**变换它们。`Position`输出的向量是一个世界空间位置向量。顶点着色器输出的`Position`用来在片段着色器计算观察方向向量。 -因为我们使用法线,你还得更新顶点数据,更新属性指针。还要确保设置cameraPos的uniform。 +因为我们使用法线,你还得更新顶点数据,更新属性指针。还要确保设置`cameraPos`的uniform。 然后在渲染箱子前我们还得绑定cubemap纹理: @@ -323,15 +323,15 @@ glBindVertexArray(0); 你可以[从这里找到全部源代码](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) 看起来挺惊艳,但是现实中大多数模型都不是完全反射的。我们可以引进反射贴图(reflection map)来使模型有另一层细节。和diffuse、specular贴图一样,我们可以从反射贴图上采样来决定fragment的反射率。使用反射贴图我们还可以决定模型的哪个部分有反射能力,以及强度是多少。本节的练习中,要由你来在我们早期创建的模型加载器引入反射贴图,这回极大的提升纳米服模型的细节。 -#### 折射 +#### 折射(refraction) -环境映射的另一个形式叫做折射,它和反射差不多。折射是光线通过特定材质对光线方向的改变。我们通常看到像水一样的表面,光线并不是直接通过的,而是让光线弯曲了一点。它看起来像当你把半只手伸进水里的效果。 +环境映射的另一个形式叫做折射,它和反射差不多。折射是光线通过特定材质对光线方向的改变。我们通常看到像水一样的表面,光线并不是直接通过的,而是让光线弯曲了一点。它看起来像你把半只手伸进水里的效果。 折射遵守[斯涅尔定律](http://en.wikipedia.org/wiki/Snell%27s_law),使用环境贴图看起来就像这样: @@ -367,15 +367,7 @@ void main() 通过改变折射指数你可以创建出完全不同的视觉效果。编译运行应用,结果也不是太有趣,因为我们只是用了一个普通箱子,这不能显示出折射的效果,看起来像个放大镜。使用同一个着色器,纳米服模型却可以展示出我们期待的效果:玻璃制物体。 -```c++ -void main() -{ - float ratio = 1.00 / 1.52; - vec3 I = normalize(Position - cameraPos); - vec3 R = refract(I, normalize(Normal), ratio); - color = texture(skybox, R); -} -``` +![](http://learnopengl.com/img/advanced/cubemaps_refraction.png) 你可以向想象一下,如果将光线、反射、折射和顶点的移动合理的结合起来就能创造出漂亮的水的图像。一定要注意,出于物理精确的考虑当光线离开物体的时候还要再次进行折射;现在我们简单的使用了单边(一次)折射,大多数目的都可以得到满足。 @@ -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) diff --git a/04 Advanced OpenGL/07 Advanced Data.md b/04 Advanced OpenGL/07 Advanced Data.md index 521b04b..589d46a 100644 --- a/04 Advanced OpenGL/07 Advanced Data.md +++ b/04 Advanced OpenGL/07 Advanced Data.md @@ -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中缓冲知识一个管理一块儿内存区域的对象,除此没有更多点的了。当把缓冲绑定到一个特定缓冲对象的时候,我们就给缓冲赋予了一个特殊的意义。当我们绑定到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++ glBufferSubData(GL_ARRAY_BUFFER, 24, sizeof(data), &data); // 范围: [24, 24 + sizeof(data)] ``` -把数据传进缓冲另一个方式是像缓冲内存请求一个指针,你自己直接把数据复制到缓冲中。调用glMapBuffer函数OpenGL会返回一个当前绑定缓冲的内存的地址,供我们操作: +把数据传进缓冲另一个方式是向缓冲内存请求一个指针,你自己直接把数据复制到缓冲中。调用`glMapBuffer`函数OpenGL会返回一个当前绑定缓冲的内存的地址,供我们操作: ```c++ float data[] = { @@ -23,37 +29,37 @@ float data[] = { }; glBindBuffer(GL_ARRAY_BUFFER, buffer); -// Get pointer +// 获取当前绑定缓存buffer的内存地址 void* ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); -// Now copy data into memory +// 向缓冲中写入数据 memcpy(ptr, data, sizeof(data)); -// Make sure to tell OpenGL we're done with the pointer +// 完成够别忘了告诉OpenGL我们不再需要它了 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。 -当从文件加载顶点数据时你通常获取一个位置数组,一个法线数组和一个纹理坐标数组。需要花点力气才能把它们结合为交叉数据。使用glBufferSubData可以简单的实现分批处理方式: +当从文件加载顶点数据时你通常获取一个位置数组,一个法线数组和一个纹理坐标数组。需要花点力气才能把它们结合为交叉数据。使用`glBufferSubData`可以简单的实现分批处理方式: ```c++ GLfloat positions[] = { ... }; GLfloat normals[] = { ... }; GLfloat tex[] = { ... }; -// Fill buffer +// 填充缓冲 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), 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))); ``` -注意,stride参数等于顶点属性的大小,由于下一个顶点属性向量可以在它的后面3(或2)的元素那儿找到。 +注意,`stride`参数等于顶点属性的大小,由于同类型的属性是连续储存的,所以下一个顶点属性向量可以在它的后面3(或2)的元素那儿找到。 这是我们有了另一种设置和指定顶点属性的方式。使用哪个方式对OpenGL来说也不会有立竿见影的效果,这只是一种采用更加组织化的方式去设置顶点属性。选用哪种方式取决于你的偏好和应用类型。 - ### 复制缓冲 + ## 复制缓冲 -当你的缓冲被数据填充以后,你可能打算让其他缓冲能分享这些数据或者打算把缓冲的内容复制到另一个缓冲里。glCopyBufferSubData函数让我们能够相对容易地把一个缓冲的数据复制到另一个缓冲里。函数的原型是: +当你的缓冲被数据填充以后,你可能打算让其他缓冲能分享这些数据或者打算把缓冲的内容复制到另一个缓冲里。`glCopyBufferSubData`函数让我们能够相对容易地把一个缓冲的数据复制到另一个缓冲里。函数的原型是: ```c++ void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset, GLintptr writeoffset, GLsizeiptr size); ``` -readtarget和writetarget参数是复制的来源和目的的缓冲目标。例如我们可以从一个VERTEX_ARRAY_BUFFER复制到一个VERTEX_ELEMENT_ARRAY_BUFFER,各自指定源和目的的缓冲目标。当前绑定到这些缓冲目标上的缓冲会被影响到。 +`readtarget`和`writetarget`参数是复制的来源和目的的缓冲目标。例如我们可以从一个`VERTEX_ARRAY_BUFFER`复制到一个`VERTEX_ELEMENT_ARRAY_BUFFER`,各自指定源和目的的缓冲目标。当前绑定到这些缓冲目标上的缓冲会被影响到。 -但如果我们打算读写的两个缓冲都是顶点数组缓冲(译注:GL_VERTEX_ARRAY_BUFFER)怎么办?我们不能把用一个缓冲目标用两次。处于这个理由,这是个例外,OpenGL给了我们另外两个缓冲目标叫做:GL_COPY_READ_BUFFER和GL_COPY_WRITE_BUFFER。这样我们就可以把我们选择的缓冲,用上面二者作为readtarget和writetarget的参数绑定到新的缓冲目标上了。 +但如果我们打算读写的两个缓冲都是顶点数组缓冲(`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++ 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)); ``` -我们也可以把writetarget缓冲绑定为新缓冲目标类型其中之一: +我们也可以把`writetarget`缓冲绑定为新缓冲目标类型其中之一: ```c++ GLfloat vertexData[] = { ... }; @@ -98,4 +104,4 @@ glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2); glCopyBufferSubData(GL_ARRAY_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData)); ``` -有了这些额外的关于如何操纵缓冲的知识,我们已经可以以更有意思的方式来使用它们了。当你对OpenGL更熟悉,这些新缓冲方法就变得更有用。下个教程中我们会讨论unform缓冲对象,彼时我们会充分利用glBufferSubData。 +有了这些额外的关于如何操纵缓冲的知识,我们已经可以以更有趣的方式来使用它们了。当你对OpenGL更熟悉,这些新缓冲方法就变得更有用。下个教程中我们会讨论unform缓冲对象,彼时我们会充分利用`glBufferSubData`。 diff --git a/04 Advanced OpenGL/08 Advanced GLSL.md b/04 Advanced OpenGL/08 Advanced GLSL.md index 10e6d80..6824308 100644 --- a/04 Advanced OpenGL/08 Advanced GLSL.md +++ b/04 Advanced OpenGL/08 Advanced GLSL.md @@ -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++ 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_Position和gl_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_FragCoord和gl_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++ @@ -77,19 +84,19 @@ void main() 因为窗口的宽是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 out vec4 color; in vec2 TexCoords; @@ -110,23 +117,23 @@ void main() ![](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数,赋值给这个输出变量: ```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++ layout (depth_) 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++ #version 330 core @@ -188,7 +195,7 @@ void main() } ``` -这次我们声明一个叫做vs_out的interface block,它把我们需要发送给下个阶段着色器的所有输出变量组合起来。虽然这是一个微不足道的例子,但是你可以想象一下,它的确能够帮助我们组织着色器的输入和输出。当我们希望把着色器的输入和输出组织成数组的时候它就变得很有用,我们会在下节几何着色器(geometry)中见到。 +这次我们声明一个叫做vs_out的接口块,它把我们需要发送给下个阶段着色器的所有输出变量组合起来。虽然这是一个微不足道的例子,但是你可以想象一下,它的确能够帮助我们组织着色器的输入和输出。当我们希望把着色器的输入和输出组织成数组的时候它就变得很有用,我们会在下节几何着色器(geometry)中见到。 然后,我们还需要在下一个着色器——片段着色器中声明一个输入interface block。块名(block name)应该是一样的,但是实例名可以是任意的。 @@ -211,13 +218,13 @@ void main() 如果两个interface block名一致,它们对应的输入和输出就会匹配起来。这是另一个可以帮助我们组织代码的有用功能,特别是在跨着色阶段的情况,比如几何着色器。 -### uniform缓冲对象 +## uniform缓冲对象 (Uniform buffer objects) 我们使用OpenGL很长时间了,也学到了一些很酷的技巧,但是产生了一些烦恼。比如说,当时用一个以上的着色器的时候,我们必须一次次设置uniform变量,尽管对于每个着色器来说它们都是一样的,所以为什么还麻烦地多次设置它们呢? OpenGL为我们提供了一个叫做uniform缓冲对象的工具,使我们能够声明一系列的全局uniform变量, 它们会在几个着色器程序中保持一致。当时用uniform缓冲的对象时相关的uniform只能设置一次。我们仍需为每个着色器手工设置唯一的uniform。创建和配置一个uniform缓冲对象需要费点功夫。 -因为uniform缓冲对象是一个缓冲,因此我们可以使用glGenBuffers创建一个,然后绑定到GL_UNIFORM_BUFFER缓冲目标上,然后把所有相关uniform数据存入缓冲。有一些原则,像uniform缓冲对象如何储存数据,我们会在稍后讨论。首先我们我们在一个简单的顶点着色器中,用uniform block(uniform块)储存projection和view矩阵: +因为uniform缓冲对象是一个缓冲,因此我们可以使用`glGenBuffers`创建一个,然后绑定到`GL_UNIFORM_BUFFER`缓冲目标上,然后把所有相关uniform数据存入缓冲。有一些原则,像uniform缓冲对象如何储存数据,我们会在稍后讨论。首先我们我们在一个简单的顶点着色器中,用uniform块(uniform block)储存投影和视图矩阵: ```c++ #version 330 core @@ -239,17 +246,17 @@ void main() 前面,大多数例子里我们在每次渲染迭代,都为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 layout(uniform块布局)。 +现在你可能会奇怪layout(std140)是什么意思。它的意思是说当前定义的uniform块为它的内容使用特定的内存布局,这个声明实际上是设置uniform块布局(uniform block layout)。 -#### uniform block layout(uniform块布局) +### uniform块布局(uniform block layout) -一个uniform block的内容更被储存到一个缓冲对象中,实际上就是在一块内存中。因为这块内存也不清楚它保存着什么类型的数据,我们就必须告诉OpenGL哪一块内存对应着色器中哪一个uniform变量。 +一个uniform块的内容被储存到一个缓冲对象中,实际上就是在一块内存中。因为这块内存也不清楚它保存着什么类型的数据,我们就必须告诉OpenGL哪一块内存对应着色器中哪一个uniform变量。 -假想下面的uniform block在一个着色器中: +假想下面的uniform块在一个着色器中: ```c++ layout (std140) uniform ExampleBlock @@ -263,71 +270,71 @@ 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布局为每个变量类型显式地声明了内存的布局。由于被显式的提及,我们就可以手工算出每个变量的偏移量。 -每个变量都有一个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. -Vector | Either 2N or 4N. This means that a vec3 has a base alignment of 4N. -Array of scalars or vectors | Each element has a base alignment equal to that of a vec4. -Matrices | Stored as a large array of column vectors, where each of those vectors has a base alignment of 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. +像int和bool这样的标量 | 每个标量的基线为N +向量 | 每个向量的基线是2N或4N大小。这意味着vec3的基线为4N +标量与向量数组 | 每个元素的基线与vec4的相同 +矩阵 | 被看做是存储着大量向量的数组,每个元素的基数与vec4相同 +结构体 | 根据以上规则计算其各个元素,并且间距必须是vec4基线的倍数 -像OpenGL大多数规范一样,举个例子就很容易理解。再次利用之前介绍的uniform块ExampleBlock,我们用std140布局,计算它的每个成员的aligned offset(对齐偏移): +像OpenGL大多数规范一样,举个例子就很容易理解。再次利用之前介绍的uniform块`ExampleBlock`,我们用std140布局,计算它的每个成员的aligned offset(对齐偏移): ```c++ layout (std140) uniform ExampleBlock { - // // base alignment // aligned offset - float value; // 4 // 0 - vec3 vector; // 16 // 16 (must be multiple of 16 so 4->16) - mat4 matrix; // 16 // 32 (column 0) - // 16 // 48 (column 1) - // 16 // 64 (column 2) - // 16 // 80 (column 3) - float values[3]; // 16 // 96 (values[0]) - // 16 // 112 (values[1]) - // 16 // 128 (values[2]) - bool boolean; // 4 // 144 - int integer; // 4 // 148 + // base alignment ---------- // aligned offset + float value; // 4 // 0 + vec3 vector; // 16 // 16 (必须是16的倍数,因此 4->16) + mat4 matrix; // 16 // 32 (第 0 行) + // 16 // 48 (第 1 行) + // 16 // 64 (第 2 行) + // 16 // 80 (第 3 行) + float values[3]; // 16 (数组中的标量与vec4相同)//96 (values[0]) + // 16 // 112 (values[1]) + // 16 // 128 (values[2]) + bool boolean; // 4 // 144 + int integer; // 4 // 148 }; ``` -尝试自己计算出偏移量,把它们和表格对比,你可以把这件事当作一个练习。使用计算出来的偏移量,根据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++ GLuint uboExampleBlock; glGenBuffers(1, &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); ``` -现在任何时候当我们打算往缓冲中更新或插入数据,我们就绑定到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++ GLuint lights_index = glGetUniformBlockIndex(shaderA.Program, "Lights"); @@ -336,40 +343,39 @@ glUniformBlockBinding(shaderA.Program, lights_index, 2); 注意,我们必须在每个着色器中重复做这件事。 -从OpenGL4.2起,也可以在着色器中通过添加另一个布局标识符来储存一个uniform block的绑定点,就不用我们调用glGetUniformBlockIndex和glUniformBlockBinding了。下面的代表显式设置了Lights这个uniform Block的绑定点: +从OpenGL4.2起,也可以在着色器中通过添加另一个布局标识符来储存一个uniform块的绑定点,就不用我们调用`glGetUniformBlockIndex`和`glUniformBlockBinding`了。下面的代表显式设置了Lights这个uniform块的绑定点: ```c++ - layout(std140, binding = 2) uniform Lights { ... }; - ``` -然后我们还需要把uniform缓冲对象绑定到同样的绑定点上,这个可以使用glBindBufferBase或glBindBufferRange来完成。 +然后我们还需要把uniform缓冲对象绑定到同样的绑定点上,这个可以使用`glBindBufferBase`或`glBindBufferRange`来完成。 ```c++ glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock); -// or +// 或者 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++ 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); glBindBuffer(GL_UNIFORM_BUFFER, 0); ``` -同样的处理也能够应用到uniform block中其他uniform变量上。 +同样的处理也能够应用到uniform块中其他uniform变量上。 -#### 一个简单的例子 +### 一个简单的例子 我们来师范一个真实的使用uniform缓冲对象的例子。如果我们回头看看前面所有演示的代码,我们一直使用了3个矩阵:投影、视图和模型矩阵。所有这些矩阵中,只有模型矩阵是频繁变化的。如果我们有多个着色器使用了这些矩阵,我们可能最好还是使用uniform缓冲对象。 -我们将把投影和视图矩阵储存到一个uniform block中,它被取名为Matrices。我们不打算储存模型矩阵,因为模型矩阵会频繁在着色器间更改,所以使用uniform缓冲对象真的不会带来什么好处。 + +我们将把投影和视图矩阵储存到一个uniform块中,它被取名为Matrices。我们不打算储存模型矩阵,因为模型矩阵会频繁在着色器间更改,所以使用uniform缓冲对象真的不会带来什么好处。 ```c++ #version 330 core @@ -388,9 +394,9 @@ void main() } ``` -这儿没什么特别的,除了我们现在使用了一个带有std140布局的uniform block。我们在例程中将显示4个立方体,每个立方体都使用一个不同的着色器程序。4个着色器程序使用同样的顶点着色器,但是它们将使用各自的片段着色器,每个片段着色器输出一个单色。 +这儿没什么特别的,除了我们现在使用了一个带有std140布局的uniform块。我们在例程中将显示4个立方体,每个立方体都使用一个不同的着色器程序。4个着色器程序使用同样的顶点着色器,但是它们将使用各自的片段着色器,每个片段着色器输出一个单色。 -首先,我们把顶点着色器的uniform block设置为绑定点0。注意,我们必须为每个着色器做这件事。 +首先,我们把顶点着色器的uniform块设置为绑定点0。注意,我们必须为每个着色器做这件事。 ```c++ 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)); ``` -我们纤维我们的缓冲分配足够的内存,它等于glm::mat4的2倍。GLM的矩阵类型的大小直接对应于GLSL的mat4。然后我们把一个特定范围的缓冲链接到绑定点0,这个例子中应该是整个缓冲。 +我们先为缓冲分配足够的内存,它等于glm::mat4的2倍。GLM的矩阵类型的大小直接对应于GLSL的mat4。然后我们把一个特定范围的缓冲链接到绑定点0,这个例子中应该是整个缓冲。 -现在所有要做的事只剩下填充缓冲了。如果我们把视野( field of view)值保持为恒定的投影矩阵(这样就不会有摄像机缩放),我们只要在程序中定义它一次就行了,这也意味着我们只需向缓冲中把它插入一次。因为我们已经在缓冲对象中分配了足够的内存,我们可以在我们进入游戏循环之前使用glBufferSubData来储存投影矩阵: +现在所有要做的事只剩下填充缓冲了。如果我们把视野( field of view)值保持为恒定的投影矩阵(这样就不会有摄像机缩放),我们只要在程序中定义它一次就行了,这也意味着我们只需向缓冲中把它插入一次。因为我们已经在缓冲对象中分配了足够的内存,我们可以在我们进入游戏循环之前使用`glBufferSubData`来储存投影矩阵: ```c++ 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); ``` -这就是uniform缓冲对象。每个包含着Matrices这个uniform block的顶点着色器豆浆邦汉uboMatrices所储存的数据。所以如果我们现在使用4个不同的着色器绘制4个立方体,它们的投影和视图矩阵都是一样的: +这就是uniform缓冲对象。每个包含着`Matrices`这个uniform块的顶点着色器都将对应`uboMatrices`所储存的数据。所以如果我们现在使用4个不同的着色器绘制4个立方体,它们的投影和视图矩阵都是一样的: ```c++ glBindVertexArray(cubeVAO); shaderRed.Use(); 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)); glDrawArrays(GL_TRIANGLES, 0, 36); -// ... Draw Green Cube -// ... Draw Blue Cube -// ... Draw Yellow Cube +// ... 绘制绿色立方体 +// ... 绘制蓝色立方体 +// ... 绘制黄色立方体 glBindVertexArray(0); ``` -我们只需要在去设置一个model的unform即可。在一个像这样的场景中使用uniform缓冲对象在每个着色器中可以减少uniform的调用。最后效果看起来像这样: +我们只需要在去设置一个`model`的uniform即可。在一个像这样的场景中使用uniform缓冲对象在每个着色器中可以减少uniform的调用。最后效果看起来像这样: ![](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)。 -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缓冲对象。