1.介绍
之前章节学习纹理的时候,我们是将外部现成的图片作为纹理,渲染到图像上。
而这里我们将要使用自己渲染的图像,作为纹理,渲染到另外一个图像上。也就是可以动态生成纹理。
2. 帧缓冲Framebuffer
虎书(fundamentals of computer graphics-5th)17章
2.1. 缓冲区
在之前的学习中碰到了许多缓冲区,为什么需要建立这么多缓冲区?
顾名思义,缓冲区就是用来缓冲的,因为GPU
的内存速度远远大于CPU
或者其他设备内存速度。我们把cpu
或者其他设备的内存称为host memory
,gpu
等显示设备的内存称为device memory
。
如果你想做一些操作,比如将host memory
的数据复制到device memory
,最典型的就是顶点缓冲数据复制到GPU
中,如果直接转移数据会因为host memory
速度太慢device memory
老是需要等待,从而影响了运行效率。
现在有了缓冲区,数据先被放入缓冲区,host memory
和device memory
各自读写(当然这中间还有很多边界问题),不会因为某一方速度而影响了另外一方。
2.2. 帧缓冲区
帧缓冲区只是逻辑上的概念,并不是真的有这么一个缓冲区,它是深度缓冲区、模板缓冲区、颜色缓冲区、积累缓冲区、多重采样缓冲区等等缓冲区共同组成的一个集合
默认情况下,系统给我们提供了一个默认的帧缓冲区。
2.3. 帧缓冲对象
要操作帧缓冲区,就必须要操作真实的缓存对象而不是逻辑的一个集合,所以诞生了帧缓冲对象FBO
。FBO
本身不存储任何数据,但是它有很多附着attachment
点,用来绑定不同的缓冲对象,实现高级的效果。
比如:
后期处理
:在帧缓冲区中渲染场景,然后将帧缓冲区的内容作为纹理传递给另一个着色器,对其进行一些后期处理,比如模糊、泛光、色调映射等。阴影贴图
:在帧缓冲区中从光源的视角渲染场景的深度值,然后将帧缓冲区的内容作为纹理传递给另一个着色器,根据深度值判断物体是否在阴影中。延迟渲染
:在帧缓冲区中渲染场景的几何信息,比如位置、法线、材质等,然后将帧缓冲区的内容作为纹理传递给另一个着色器,根据几何信息计算光照和着色。
上图展示了一个FBO
的结构:
FBO
包含多个附着点,至少一个颜色缓冲区附着点(color_attachment),一个深度缓冲区附着点(depth_attachment),一个模板缓冲区附着点(stencil_attachment)。- 每个附着点都可以绑定两种对象:纹理对象Texture Object、渲染缓冲对象RenderBuffer Object
- 颜色附着点有多个的原因:因为渲染结果的颜色值可以有多种格式和通道,比如
RGB
、RGBA
、RGB16F
等,而且可以使用MRT(多渲染目标)
技术使着色器能够输出不同信息给多个缓冲区,就像这样:
- 纹理对象和渲染缓冲对象区别,找到的信息比较少,大概就是渲染缓冲对象只能读写且存储的数据格式是
opengl
原始格式,无法采样或者更细致的操作,但是由于渲染缓冲对象更简单,存储的速度更快,操作耗时也更少。
3. 渲染到纹理
现在试着把在帧缓冲区绘制好的图像当成纹理,渲染到另外一个图像上。
我们将自定义FBO
,替代默认的帧缓冲区。
我们先在自定义的帧缓冲上绘制,再替换默认的缓冲区。在替换之前的绘制被称之为离屏绘制
。这样可以提高效率,毕竟是在内存中操作片元,就像dom
中的fragment
。
就像这样,我把一个立方体当做纹理渲染到了一个平面上。
3.1. 创建帧缓冲
相当于创建了一个新的绘制空间,只不过不显示在屏幕上。
// 1. 首先绑定新的帧缓冲
const framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
// 离屏绘制区域大小
// webgl1的texImage2D只能是2的次幂宽高
const OFFSCREEN_WIDTH = 256
const OFFSCREEN_HEIGHT = 256
gl.viewport(0, 0, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT);
上面绑定了一个新的帧缓冲区域,默认的帧缓冲区域是gl.bindFramebuffer(gl.FRAMEBUFFER, null)
,然后设置了viewport
,也就是绘制区域,越小绘制越快但是更模糊。
// 2. 将纹理对象绑定到颜色缓冲附着点
const framebufferTexture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, framebufferTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, framebufferTexture, 0);
上面绑定了帧缓冲的一个颜色缓冲附着点,我们将一个纹理对象绑定在上面,这个纹理对象很重要,之后的绘制相当于就是在这个对象上绘制。
texImage2D
中的最后一个参数,在之前的先学习中直接给的image
标签,现在不需要绑定数据,因为数据是后面绘制上去的。
在webgl 1
中只有一个颜色附着点,所以framebufferTexture2D
中只能绑定给COLOR_ATTACHMENT0
这里补充一点之前在纹理学习中没有的点:
在WebGL 1
中,texImage2D
的宽高必须是2的次幂,否则会报错。这是因为WebGL 1
只支持幂次方纹理(power-of-two textures),即纹理的宽度和高度都是2的次幂。如果你想使用非幂次方纹理(non-power-of-two textures),需要在WebGL 2
中使用,并且满足以下条件:
- 纹理过滤方式必须是
gl.NEAREST
或gl.LINEAR
,不能使用mipmap
。 - 纹理包裹方式必须是
gl.CLAMP_TO_EDGE
,不能使用gl.REPEAT
或gl.MIRRORED_REPEAT
。 - 纹理的
level
参数必须是0
,不能使用多级分辨率。
// 3. 将渲染缓冲区绑定到深度缓冲附着点
const depthBuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
因为我们要绘制三维图形,所以需要深度信息,所以这里将渲染缓冲对象绑定到深度缓冲附着点。
这里要注意深度缓冲区的大小要和颜色缓冲区大小一样。
3.2 绘制纹理图像
上面的准备工作相当于新建了一个绘制的空间,里面有全新的深度缓冲区和颜色缓冲区。
// 绘制立方体
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
drawNormalCube()
上面代码值得注意的是,清空之前绑定的纹理对象bindTexture
,因为此时绘制的图像不是贴上framebufferTexture
,而是写入framebufferTexture
。
总之在初始化帧缓冲之后应该先清空绑定,这样可以避免未知现象。
3.3. 绘制纹理
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.bindTexture(gl.TEXTURE_2D, framebufferTexture);
drawPanel()
上面代码恢复了默认帧缓冲,然后重新设置了viewport
,最后将之前的帧缓冲的纹理对象作为纹理绘制上去。