CSharpGL(47)你好,Framebuffer!
Framebuffer对象(FBO)是一种复杂的OpenGL对象。使用自定义的framebuffer,可以实现离屏渲染,进而实现很多高级功能,例如阴影。
下载
CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/CSharpGL)
FBO基本结构
【注:本节(FBO基本结构)是翻译的(https://www.khronos.org/opengl/wiki/Framebuffer_Object),略有修改。】
类似其它的OpenGL对象,FBO也有一套glGen, glDelete, glBind的API。
FBO这套API里的target可接受3种值:GL_FRAMEBUFFER, GL_READ_FRAMEBUFFER或GL_DRAW_FRAMEBUFFER。后两种允许你可以让读操作(glReadPixels等)和写操作(所有的渲染命令)发生到不同的FBO上。GL_FRAMEBUFFER 则将读写发生到同一个FBO。
名词术语
为了叙述方便,首先定义一些术语。
Image
像素的二维数组(Pixel[ , ]),有特定的格式。
Layered Image
相同大小和格式的一组Image。
Texture
包含若干Image的OpenGL对象。这些Image的格式相同,但大小未必相同(例如不同mipmap level的Image大小是不同的)。Texture可以以多种方法被shader读取。
Renderbuffer
包含1个Image的OpenGL对象。不能被shader读取。只能被创建,然后放到FBO里。
Attach
把一个对象关联(附着)到另一个对象上。附着attach不同于绑定binding。对象被绑定到上下文context,对象被附着到另一个对象。
Attachment point
Framebuffer对象里可以让Image或Layered Image附着的位置。只有符合规定的图像格式才能被附着。
Framebuffer-attachable image
格式符合规定,可以被附着到framebuffer对象的Image。
Framebuffer-attachable layered image
格式符合规定,可以被附着到framebuffer对象的Layered Image。
附着点Attachment Point
FBO有若干Image的附着点(位置):
GL_COLOR_ATTACHMENTi
这些附着点的数量依不同的实现而不同。你可以用GL_MAX_COLOR_ATTACHMENTS 查询一个OpenGL实现支持的颜色附着点的数量。最少有8个,所以你最少可以放心使用附着点0-7。这些附着点只能让可渲染色彩的Image来附着。所有compressed image formats都不是可渲染色彩的,所以都不能附着到FBO。
GL_DEPTH_ATTACHMENT
这个附着点只能让depth格式的Image附着。附着的Image就成了此FBO的depth buffer。**注意**,即使你不打算从深度附着点上读取什么东西,也应该给深度附着点设定一个Image。
GL_STENCIL_ATTACHMENT
这个附着点只能让stencil格式的Image附着。附着的Image就成了此FBO的stencil buffer。
GL_DEPTH_STENCIL_ATTACHMENT
这是“depth+stencil”的简写。附着的Image既是depth buffer又是stencil buffer。注意:如果你使用GL_DEPTH_STENCIL_ATTACHMENT,你应当使用一个以packed depth-stencil为内部格式的Texture或Renderbuffer。
Attaching Images
现在我们已经知道了Image可以附着到FBO的哪些位置上,我们可以开始谈谈如何将Image附着到FBO上。首先,我们必须用glBindFramebuffer把FBO绑定到context。
Attaching Texture
首先来了解一下各种类型的Texture:
图中列出了8种类型的Texture。上方分别是1D Texture、2D Texture、3D Texture和2D Array Texture,下方分别是1D Array、Cubemap Texture、Rectangle Texture和Buffer Texture。大多数Texture都支持mipmap(上图中每个Texture从上到下分别为mipmap level0,1,2,3…)。
你可以将基本上任何类型的Texture里的Image附着到FBO。不过,FBO是被设计来做2D渲染的。所以有必要考虑一下不同类型的Texture是如何映射到FBO里的Image的。记住,Texture就是一组Image,Texture可能包含多个mipmap level,每个mipmap level都可能包含1到多个Image。
然后,对照上图,不同类型的Texture映射到FBO里的Image的方式如下:
1D Texture里的Image被视作高度为1的2D Image。1个Image可以被mipmap level标识。
2D Texture里的Image就照常使用了。1个Image可以被mipmap level标识。
3D Texture的1个mipmap level被视作2D Image的集合,此集合的元素数量即为此mipmap level的Z坐标。Z坐标的每个整数值都是一个单独的2D层(layer)。所以3D Texture里的的一个Image由layer和mipmap level共同标识。记得3D Texture的不同mipmap level的Z坐标数量是不同的。
Rectangle Textures只有1个2D Image,因此直接用mipmap level 0标识。
Cubemap Textures里每个mipmap都包含6个2D Image。因此,1个Image可以被面target和mipmap level标识。然而有些API函数里,1个mipmap level里的各个face是用layer索引标识的。
1D或2D Array Textures的每个mipmap level都包含多个Image,其数量等于数组元素的数量。因此,每个Image可以被layer(数组索引)和mipmap level标识。1D Array Texture里,每个Image都是高度为1。与3D Texture不同的是,layer不随mipmap层的递进而改变。(即各个mipap level的layer数量都相同)
Cubemap Array Textures类似2D Array Texture,只是Image数量乘以6。因此一个2D Image由layer(具体的说是layer-face)和mipmap level标识。(这个太难画我就不画了)
Buffer Textures 不能被附着到FBO。
上面带下划线的字很重要,因为他们对应了下面的API函数(用于附着Texture)的参数:
1 void glFramebufferTexture1D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level); 2 void glFramebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level); 3 void glFramebufferTextureLayer(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer);
参数target与glBind用的相同。但是这里GL_FRAMEBUFFER的意思不是“既可读又可写”(那没有意义),他是和GL_DRAW_FRAMEBUFFER相同的意思。参数attachment是上面介绍的附着点。
参数texture是你想要附着到FBO的的Texture的名字。如果你传入“0”,就会清除指定的attachment位置上的附着物(不管附着物是什么)。
因为Texture可能包含多个Image,你必须详细说明要将哪个Image附着到附着点。除textarget之外,参数都符合上文的定义。
当附着一个非cubemap的Texture时,textarget应当是合适的类型:GL_TEXTURE_1D, GL_TEXTURE_2D_MULTISAMPLE等。当附着一个非数组的cubemap时,你必须使用glFramebufferTexture2D函数,且textarget必须是cubemap binding的6个target之一。当附着一个cubemap array时,你必须使用TextureLayer,用layer标识layer-face。
注意:如果OpenGL4.5或ARB_direct_state_access可用,那么glFramebufferTextureLayer可以接受非数组cubemap类型的Texture。他会被视作只有1个layer(即6个layer-face)的数组cubemap类型的Texture。这意味着你永远不需要使用glFramebufferTexture2D或者glFramebufferTexture1D。
又注意:有一个函数glFramebufferTexture3D,专用于3D Texture。但是你不应该使用他,因为TextureLayer函数能够完成他所有的功能。
Attaching Renderbuffer
Renderbuffers也可以被附着到FBO。实际上,这也是除了创建他们之外唯一的使用方法。
1 void glFramebufferRenderbuffer(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer);
参数与附着Texture的类似。参数renderbuffertarget必须是GL_RENDERBUFFER,参数renderbuffer是renderbuffer的名字。
Layered Images
Layered Image,如前所述,是一组有序的大小相同的Image。多种Texture都可以被认为是layered。
1D或2D Array Texture的1个mipmap level就是一个Layered Image,数组的元素数就是层数。3D Texture的1个mipmap level同样也是一个Layered Image,层数就是此mipmap level的depth。Cubemap Texture的1个mipmap level也是一个Layered Image,他有且只有6个layer,每个face是一个,且face的顺序与下面的枚举值相同:
Layer number |
Cubemap face |
0 |
GL_TEXTURE_CUBE_MAP_POSITIVE_X |
1 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_X |
2 |
GL_TEXTURE_CUBE_MAP_POSITIVE_Y |
3 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y |
4 |
GL_TEXTURE_CUBE_MAP_POSITIVE_Z |
5 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z |
对于cubemap array texture,Layer 代表的是layer-face的索引。他是带layer的face,按上表排列。所以如果你想渲染到第三个layer的+z face,你就要设置gl_Layer 为(2 * 6) + 4或者16。
每个Texture,被用作Layered Image的时候,都有特定数量的layer。对于Array Texture或3D Texture,layer数就是Texture 的depth。对于cubemap,总是有且只有6个layer:每个face即为1个layer。Cubmap Array 有6*layer(layer-face数)。
使用下述指令可以将Texture的一个mipmap level附着为一个Layered Image:
1 void glFramebufferTexture(GLenum target, GLenum attachment, GLuint texture, GLint level);
参数含义与上文的相同。实际上,如果你不要求附着Array Texture, Cubemap或3D Texture的单独一个Image,那么这个函数可以代替很多glFramebufferTexture1D,2D或Layer。但如果texture是这种情况,那么给定的整个mipmap level将作为一个Layered Image整体被附着,即此Layered Image里所有的layer都会被附着。(译者注:有什么用呢?下面立即分解)
Layered Image用于Layered Rendering,即向FBO的不同Layer发送不同的图元(在同一次渲染中形成不同的图像)。
Empty framebuffers
有时候会需要向一个没有附着对象的FBO渲染。显然fragment shader的输出不会写入到任何地方,但是渲染过程还是可以正常进行的。这对于shader的arbitrary reading and writing of image data是有用的。
但是,图元的渲染总是基于FBO的性质(大小,sample数量等)进行的,这些性质通常由被附着的Image定义。如果没有附着Image,这些性质就必须用其它的方式定义。
没有附着Image的FBO的性质可以用下述函数设置:
1 void glFramebufferParameteri(GLenum target, GLenum pname, GLint param);
target是FBO绑定的位置。如果要设置width,就设pname为GL_FRAMEBUFFER_DEFAULT_WIDTH;,如果要设置height,就设pname为GL_FRAMEBUFFER_DEFAULT_HEIGHT。
Layered FBO可以通过设置GL_FRAMEBUFFER_DEFAULT_LAYERS 为大于0的值来模仿。Multisample FBO可以通过设置GL_FRAMEBUFFER_DEFAULT_SAMPLES 为大于0的值来模仿。Fixed multisample位置可以通过设置GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS 为非零值来模仿。
注意,仅在FBO没有附着对象的时候,这些参数才会起作用。如果附着了Image,那么这些参数会被无视。你应该仅在你想要使用无Image的FBO时才设置这些值。
Framebuffer Completeness
FBO里每个附着点都对能附着的Image的格式有要求。但是,如果附着了不符合要求的Image,不会立即产生GL error。在使用不合适的设置的FBO时才会引发错误。为了安全地使用FBO,必须检测各种可能出现的问题(例如Image的大小等)。
一个可以正常使用的FBO被称作是“完整的FBO”。想要测试FBO的完整性,请调用这个函数:
1 GLenum glCheckFramebufferStatus(GLenum target);
你不是非得调用这个函数不可。但是,使用不完整的FBO是错误的,所以检测一下总是好的。
如果FBO能用,会返回GL_FRAMEBUFFER_COMPLETE 。否则就是有问题。
FBO in C#
FBO最复杂的操作就是Attach不同类型的Texture。根据上文,可以总结出来,只需要glFramebufferTexture和glFramebufferTextureLayer两个函数就可以实现对所有类型Texture的Attach的支持。Wiki说OpenGL3.2开始才支持glFramebufferTexture,这我就不管了。
1 /// <summary> 2 /// Attach a level of the <paramref name="texture"/> as a logical buffer to the currently bound framebuffer object. 3 /// If there are multiple images in one mipmap level of the <paramref name="texture"/>, then we will start 'layered rendering'. 4 /// <para>Bind() this framebuffer before invoking this method.</para> 5 /// </summary> 6 /// <param name="target">GL_FRAMEBUFFER is equivalent to GL_DRAW_FRAMEBUFFER</param> 7 /// <param name="texture">Specifies the texture object to attach to the framebuffer attachment point named by <paramref name="location"/>.</param> 8 /// <param name="location">Specifies the attachment point of the framebuffer.</param> 9 /// <param name="mipmapLevel">Specifies the mipmap level of <paramref name="texture"/> to attach.</param> 10 public void Attach(FramebufferTarget target, Texture texture, AttachmentLocation location, int mipmapLevel = 0) 11 { 12 if (texture == null) { throw new ArgumentNullException("texture"); } 13 14 if (location == AttachmentLocation.Color) 15 { 16 if (this.nextColorAttachmentIndex >= Framebuffer.maxColorAttachmentCount) 17 { throw new IndexOutOfRangeException("Not enough color attach points!"); } 18 19 glFramebufferTexture((uint)target, GL.GL_COLOR_ATTACHMENT0 + this.nextColorAttachmentIndex, texture != null ? texture.Id : 0, mipmapLevel); 20 this.nextColorAttachmentIndex++; 21 } 22 else 23 { 24 glFramebufferTexture((uint)target, (uint)location, texture != null ? texture.Id : 0, mipmapLevel); 25 } 26 } 27 28 /// <summary> 29 /// Attach a single layer of a <paramref name="cubemapArrayTexture"/> to the currently bound framebuffer object. 30 /// <para>Bind() this framebuffer before invoking this method.</para> 31 /// </summary> 32 /// <param name="target">GL_FRAMEBUFFER is equivalent to GL_DRAW_FRAMEBUFFER</param> 33 /// <param name="cubemapArrayTexture">texture must either be null or an existing cube map array texture.</param> 34 /// <param name="location">attachment point.</param> 35 /// <param name="layer">Specifies the layer of <paramref name="cubemapArrayTexture"/> to attach.</param> 36 /// <param name="face">Specifies the face of <paramref name="cubemapArrayTexture"/> to attach.</param> 37 /// <param name="mipmapLevel">Specifies the mipmap level of <paramref name="cubemapArrayTexture"/> to attach.</param> 38 public void Attach(FramebufferTarget target, Texture cubemapArrayTexture, AttachmentLocation location, int layer, CubemapFace face, int mipmapLevel = 0) 39 { 40 this.Attach(target, cubemapArrayTexture, location, (layer * 6 + (int)((uint)face - GL.GL_TEXTURE_CUBE_MAP_POSITIVE_X)), mipmapLevel); 41 } 42 43 /// <summary> 44 /// Attach a single layer of a <paramref name="texture"/> to the currently bound framebuffer object. 45 /// <para>Bind() this framebuffer before invoking this method.</para> 46 /// </summary> 47 /// <param name="target">GL_FRAMEBUFFER is equivalent to GL_DRAW_FRAMEBUFFER</param> 48 /// <param name="texture">texture must either be null or an existing three-dimensional texture, one- or two-dimensional array texture, cube map array texture, or multisample array texture.</param> 49 /// <param name="location">attachment point.</param> 50 /// <param name="layer">Specifies the layer of <paramref name="texture"/> to attach.</param> 51 /// <param name="mipmapLevel">Specifies the mipmap level of <paramref name="texture"/> to attach.</param> 52 public void Attach(FramebufferTarget target, Texture texture, AttachmentLocation location, int layer, int mipmapLevel = 0) 53 { 54 if (location == AttachmentLocation.Color) 55 { 56 if (this.nextColorAttachmentIndex >= Framebuffer.maxColorAttachmentCount) 57 { throw new IndexOutOfRangeException("Not enough color attach points!"); } 58 59 glFramebufferTextureLayer((uint)target, GL.GL_COLOR_ATTACHMENT0 + this.nextColorAttachmentIndex, texture != null ? texture.Id : 0, mipmapLevel, layer); 60 this.nextColorAttachmentIndex++; 61 } 62 else 63 { 64 glFramebufferTextureLayer((uint)target, (uint)location, texture != null ? texture.Id : 0, mipmapLevel, layer); 65 } 66 }
总结
。