在之前的章节中,我们已经接触过缓冲的概念了,比如顶点的坐标、颜色、法向量、纹理坐标等,今天我们学习一个新的缓冲概念:帧缓冲。
概念
顾名思义,帧缓冲(Frame Buffer Object)也是一个缓冲对象,不同于之前的缓冲,它相当于一个存在于内存中的不可见画布,我们可以先将即将绘制的内容绘制到帧缓冲中,然后对其做一些处理,之后,再将其绘制到画布上,这种方式让我们能够针对场景进行后处理,实现一些场景特效。
在之前的绘制过程中,渲染操作也是有帧缓冲的,只不过使用的是系统默认的帧缓冲。
显然,帧缓冲既然也是一块画布,那么它经常要和颜色打交道。因此,帧缓冲通常至少包含一个颜色缓冲区,除此之外,我们还需要一个图像载体,将图像绘制到载体上,载体分为两种,纹理
和渲染缓冲对象
。
纹理的优势是,我们可以在着色器中使用这个纹理,然后对其像素做后期处理。
渲染缓冲对象不能在着色器中使用,但是它有纹理不支持的特性,最大优点就是渲染缓冲所包含的各种数据是已经优化过的。
简单来说,帧缓冲就像提供给我们一个渲染前的画板,我们先在该画板上画好要显示的图像,满意之后再将它输出到屏幕上。
实战
我们将一幅图像用 2D 纹理渲染到自定义帧缓冲,同时将该自定义缓冲渲染到一个2D纹理,接下来再将帧缓冲对象绑定到默认帧缓冲,在默认帧缓冲上将上一步的纹理映射到一个立方体上,并在屏幕显示出来。
创建帧缓冲对象
首先创建帧缓冲对象,然后将该对象设置为绑定到帧缓冲绑定点
。
let frameBuffer = gl.createFrameBuffer();
gl.bindFrameBuffer(gl.FRAMEBUFFER, frameBuffer);
创建帧缓冲图像的写入纹理
接下来,我们创建一个纹理对象,并设置帧缓冲向纹理写入数据时的参数,最后将帧缓冲和纹理进行关联。
创建帧缓冲纹理
let frameTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, frameTexture);
设置帧缓冲向纹理写入数据时的参数:
// 绑定到第一个颜色附加区。
let attachmentPoint = gl.COLOR_ATTACHMENT0;
// 写入数据,注意初始化时应为 null。
let data = null;
// 绑定帧缓冲纹理作为当前纹理操作对象。
gl.bindTexture(gl.TEXTURE_2D, frameTexture);
// 设置写入参数,256 代表纹理的宽和高。
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA,256, 256, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
// 设置放大或者缩小时的算法
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 设置超出边界时的算法
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// 将纹理和帧缓冲绑定。
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, targetTexture, 0);
做完这几步,当我们执行完drawArrays等渲染操作之后,帧缓冲中的数据就渲染到该纹理对象上了。
绘制帧缓冲
接下来,我们创建一个专门向帧缓冲纹理进行绘制的方法,该方法将向自定义缓冲渲染两个立方体。
function drawFrame(){
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
gl.clearColor(1,1,0,1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0, 0, 256, 256);
...绘制立方体,此处略。
}
你会发现,我们的每一个操作步骤都是有目的的:
- 首先将自定义帧缓冲对象绑定到帧缓冲绑定点上。
- 之后设置清屏颜色,进行颜色和深度信息的清除处理,主要是为了和画布的绘制进行区分。
- 最后还需要重新设置绘图区域大小,这步也很关键,不然你会发现渲染到纹理上的图像和我们期待的不一样。
绘制到画布
我们在默认缓冲绘制一个立方体,立方体每个面使用的纹理采用自定义缓冲渲染到的纹理。
gl.bindFramebuffer(gl.FRAMEBUFFER,null);
gl.bindTexture(gl.TEXTURE_2D, frameTexture);
gl.uniform1i(useTexture, true);
gl.clearColor(1, 1, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.uniform1i(u_Skybox, 0);
...绘制立方体,此处略
你会发现,我们的步骤和渲染到纹理步骤有所区别:
- 首先将帧缓冲绑定到默认缓冲上。
- 其次绑定纹理对象为帧缓冲纹理,因为我们要将自定义帧缓冲渲染到立方体的每一个面上。
- 接着设置常量useTexture 为true,设置它为true是时才使用纹理。
- 设置清屏颜色,并进行清屏。
- 重新设置绘图区域。
- 将零号纹理传到着色器里。
效果
这就是最终渲染的效果,立方体的每个面上被自定义帧缓冲上的对象所填充。
加入深度缓冲
上面的例子,我们只为帧缓冲附加了颜色缓冲信息,事实上,帧缓冲还可以附加深度缓冲、模板缓冲。接下来,我们为帧缓冲加入深度缓冲信息。 我们用渲染缓冲对象保存深度缓冲信息,那么首先要创建渲染缓冲对象。
// 创建一个深度缓冲
const renderBuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, renderBuffer);
接下来,设置渲染缓冲对象的大小,这里和前面创建的纹理大小保持一致。
// 设置深度缓冲的大小和帧缓冲纹理一致。
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, 256, 256);
最后,将渲染缓冲对象和帧缓冲对象进行关联。
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderBuffer);
效果
可以看到,两个立方体的深度信息显现出来了。
应用
那么,帧缓冲有什么好处呢?做了这个例子,大家心里也许已经有一些想法了。
在做一些赛车游戏时,如果玩家想从反光镜中看车后面的景色,这时候,帧缓冲就派上用场了。
再有,当我们需要对场景进行后期处理时,我们就可以 用帧缓冲渲染到纹理的方式,对纹理像素进行处理,实现某些特效,比如反向、模糊、黑白处理等。
回顾
以上就是帧缓冲的内容,概念有些抽象,但是大家可以通过一些例子来感受它的存在。
至此,小册内容就结束了,下一节,我会对所学内容做一个总结。