目录javascript
事物是广泛联系的。为了达到更加真实的渲染效果,不少时候须要利用被渲染物体在其余状态下的中间渲染结果,处理到最终显示的渲染场景中。这种中间渲染结果,就保存在帧缓冲区对象(framebuffer object,简称FBO)中,用来替代颜色缓冲区或深度缓存区。因为其结果并不直接被显示出来,因此这种技术也被称为离屏绘制(offscreen drawing)。html
在以前的教程实例中,地形的颜色信息都是来自于顶点缓冲区对象。而在这篇教程中,准备写出这样一个示例:分别在帧缓冲区和颜色缓冲区中绘制同一块地形,颜色缓冲区的颜色信息不经过顶点缓冲区获取而经过帧缓冲区获取。这个简单的示例并无具体的实际意义,可是能更好的理解FBO,FBO是后续更高级技术的基础。java
示例的完整代码太长,这里就不放出来了,能够在文章尾部提供的地址自行下载;这里主要讲解其中的关键部分。git
这里定义了两组着色器,一组是绘制在帧缓冲区的:github
// 顶点着色器程序-绘制到帧缓存 var FRAME_VSHADER_SOURCE = 'attribute vec4 a_Position;\n' + //位置 'attribute vec4 a_Color;\n' + //颜色 'uniform mat4 u_MvpMatrix;\n' + 'varying vec4 v_Color;\n' + 'void main() {\n' + ' gl_Position = u_MvpMatrix * a_Position;\n' + // 设置顶点坐标 ' v_Color = a_Color;\n' + '}\n'; // 片元着色器程序-绘制到帧缓存 var FRAME_FSHADER_SOURCE = 'precision mediump float;\n' + 'varying vec4 v_Color;\n' + 'void main() {\n' + ' gl_FragColor = v_Color;\n' + //将深度保存在FBO中 '}\n';
能够看到这段着色器程序与绘制在颜色缓冲区的着色器没有区别。另一组是正常绘制在颜色缓冲区的:web
// 顶点着色器程序 var VSHADER_SOURCE = 'attribute vec4 a_Position;\n' + //位置 'attribute vec4 a_Color;\n' + //颜色 'attribute vec4 a_Normal;\n' + //法向量 'uniform mat4 u_MvpMatrix;\n' + 'varying vec4 v_PositionFromLight;\n' + 'void main() {\n' + ' gl_Position = u_MvpMatrix * a_Position;\n' + ' v_PositionFromLight = gl_Position;\n' + '}\n'; // 片元着色器程序 var FSHADER_SOURCE = '#ifdef GL_ES\n' + 'precision mediump float;\n' + '#endif\n' + 'uniform sampler2D u_Sampler;\n' + //颜色贴图 'varying vec4 v_PositionFromLight;\n' + 'void main() {\n' + //获取颜色贴图中的值 ' vec3 shadowCoord = (v_PositionFromLight.xyz/v_PositionFromLight.w)/2.0 + 0.5;\n' + ' gl_FragColor = texture2D(u_Sampler, shadowCoord.xy);\n' + '}\n';
这里能够看到最终位置仍然来自顶点数组,颜色倒是从一个纹理对象插值出来的。这个纹理对象正是帧缓冲区中关联的纹理对象,它是在帧缓冲对象绘制以后传递过来的。编程
注意这里关于纹理坐标的计算,在《WebGL简易教程(五):图形变换(模型、视图、投影变换)》这篇教程中曾经提到过,在通过顶点着色器以后,顶点坐标会归一化到-1到1之间;而纹理坐标是在0到1之间的,因此这里须要坐标变换一下。canvas
首先仍然是进行一些初始化操做。获取上下文后建立着色器,并初始化帧缓冲对象(FBO):数组
// 获取 <canvas> 元素 var canvas = document.getElementById('webgl'); // 获取WebGL渲染上下文 var gl = getWebGLContext(canvas); if (!gl) { console.log('Failed to get the rendering context for WebGL'); return; } //初始化两个着色器,drawProgram绘制到界面,frameProgram绘制到帧缓存 var drawProgram = createProgram(gl, VSHADER_SOURCE, FSHADER_SOURCE); var frameProgram = createProgram(gl, FRAME_VSHADER_SOURCE, FRAME_FSHADER_SOURCE); if (!drawProgram || !frameProgram) { console.log('Failed to intialize shaders.'); return; } //从着色器中获取地址,保存到对应的变量中 GetProgramLocation(gl, drawProgram, frameProgram); // 初始化帧缓冲区对象 (FBO) var fbo = initFramebufferObject(gl); if (!fbo) { console.log('Failed to intialize the framebuffer object (FBO)'); return; } // 开启深度测试 gl.enable(gl.DEPTH_TEST); // 指定清空<canvas>的颜色 gl.clearColor(0.0, 0.0, 0.0, 1.0); //清空颜色和深度缓冲区 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
这里的函数GetProgramLocation是功能将从着色器获取的数据地址保存起来,由于涉及到一些切换着色器再分配数据的操做,保存到变量中方便一些:缓存
//从着色器中获取地址,保存到对应的变量中 function GetProgramLocation(gl, drawProgram, frameProgram) { // Get the storage location of attribute variables and uniform variables drawProgram.a_Position = gl.getAttribLocation(drawProgram, 'a_Position'); drawProgram.u_MvpMatrix = gl.getUniformLocation(drawProgram, 'u_MvpMatrix'); if (drawProgram.a_Position < 0 || !drawProgram.u_MvpMatrix) { console.log('Failed to get the storage location of a_Position, u_MvpMatrix'); //return; } frameProgram.a_Position = gl.getAttribLocation(frameProgram, 'a_Position'); frameProgram.a_Color = gl.getAttribLocation(frameProgram, 'a_Color'); frameProgram.u_MvpMatrix = gl.getUniformLocation(frameProgram, 'u_MvpMatrix'); if (frameProgram.a_Position < 0 || frameProgram.a_TexCoord < 0 || !frameProgram.u_MvpMatrix) { console.log('Failed to get the storage location of a_Position, a_Color, u_MvpMatrix'); //return; } }
在示例中实际进行了两次绘制操做,分别在帧缓冲区和颜色缓冲区中绘制了一遍。所以,须要用到两组不一样的着色器。可是同一时间内只能用一组着色器进行绘制工做,这里就涉及到一个着色器切换的问题。
在以前的例子当中,都是经过WebGL组件cuon-utils中的函数initShaders来初始化着色器。这个函数实际上包含了建立着色器程序功能函数createProgram(),以及设置当前着色器函数gl.useProgram():
function initShaders(gl, vshader, fshader) { var program = createProgram(gl, vshader, fshader); if (!program) { console.log('Failed to create program'); return false; } gl.useProgram(program); gl.program = program; return true; }
在程序初始化的时候只须要建立着色器函数createProgram()就能够了,在须要传输数据和绘制的时候再去设置当前的着色器gl.useProgram()。
除此以外,顶点缓冲区的使用也有所改变。在以前的教程《WebGL简易教程(三):绘制一个三角形(缓冲区对象)》中介绍过使用顶点缓冲区的五个步骤:
可是为了节省空间,两个不一样的着色器是使用相同的顶点缓冲区数据,在须要的时候切换分配数据。所以这里能够将以上五步分红两个函数——在初始化的时候,进行1~3步:向顶点缓冲区写入数据,留待绘制的时候分配使用:
//向顶点缓冲区写入数据,留待之后分配 function initArrayBufferForLaterUse(gl, data, num, type) { // Create a buffer object var buffer = gl.createBuffer(); if (!buffer) { console.log('Failed to create the buffer object'); return null; } // Write date into the buffer object gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); // Store the necessary information to assign the object to the attribute variable later buffer.num = num; buffer.type = type; return buffer; }
在绘制时切换到对应的着色器,进行4~5步:分配缓冲区对象并开启链接:
//分配缓冲区对象并开启链接 function initAttributeVariable(gl, a_attribute, buffer) { gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.vertexAttribPointer(a_attribute, buffer.num, buffer.type, false, 0, 0); gl.enableVertexAttribArray(a_attribute); }
固然,顶点数据索引也同时分配到顶点缓冲区,须要的时候绑定缓冲区对象便可:
//向顶点缓冲区写入索引,留待之后分配 function initElementArrayBufferForLaterUse(gl, data, type) { // Create a buffer object var buffer = gl.createBuffer(); if (!buffer) { console.log('Failed to create the buffer object'); return null; } // Write date into the buffer object gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW); buffer.type = type; return buffer; }
帧缓冲区对象保存的是渲染的中间结果,所以分别存在三个关联对象——颜色关联对象(color attachment)、深度关联对象(depth attachment)和模板关联对象(stencil attachment),用来代替颜色缓冲区、深度缓冲区和模板缓冲区。关联对象分为两种:纹理对象和渲染缓冲区对象(renderbuffer object)。通常来讲,能够定义一个纹理对象做为帧缓冲区的的颜色关联对象,定义一个渲染缓冲区对象做为帧缓冲区的深度关联对象,来实现离屏绘制。
在函数initFramebufferObject()中进行了帧缓冲区的初始化工做。具体来讲, 帧缓冲区的具体设置过程能够分为以下8步:
经过gl.createFramebuffer()来建立初始化对象:
// 初始化帧缓冲区对象 (FBO) function initFramebufferObject(gl) { //... // 建立帧缓冲区对象 (FBO) framebuffer = gl.createFramebuffer(); if (!framebuffer) { console.log('Failed to create frame buffer object'); return error(); } //... }
在教程《WebGL简易教程(十一):纹理》中就已经介绍过如何建立纹理对象并设置纹理对象的参数。这里的建立过程也是同样的;只是细节略有不一样:
function initFramebufferObject(gl) { //... // 建立纹理对象并设置其尺寸和参数 texture = gl.createTexture(); // 建立纹理对象 if (!texture) { console.log('Failed to create texture object'); return error(); } gl.bindTexture(gl.TEXTURE_2D, texture); // Bind the object to target gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); // 设置纹理参数 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_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); framebuffer.texture = texture; // 保存纹理对象 //... }
经过函数gl.createRenderbuffer()建立渲染缓冲区对象,这个渲染缓冲区对象将被指定成深度关联对象。
function initFramebufferObject(gl) { //... // 建立渲染缓冲区对象并设置其尺寸和参数 depthBuffer = gl.createRenderbuffer(); //建立渲染缓冲区 if (!depthBuffer) { console.log('Failed to create renderbuffer object'); return error(); } //... }
将渲染缓冲区绑定到目标上,经过目标设置渲染缓冲区的尺寸等参数。
function initFramebufferObject(gl) { //... gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); // Bind the object to target gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT); //... }
对于WebGL/OpenGL而言,任何缓冲区对象都是须要绑定到目标上,再对目标进行操做的。绑定函数gl.bindRenderbuffer()的定义为:
绑定完成后,经过gl.renderbufferStorage()函数设置渲染缓冲区的格式、宽度以及高度等。注意深度关联的渲染缓冲区,其宽度和高度必须与做为颜色关联对象的纹理缓冲区一致。其函数定义为:
仍然是先将帧缓冲绑定到目标上,使用函数gl.bindFramebuffer()进行绑定:
function initFramebufferObject(gl) { //... // 将纹理和渲染缓冲区对象关联到帧缓冲区对象上 gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); //关联颜色 //... }
注意这里的attachment参数的取值gl.COLOR_ATTACHMENT0,WebGL和OpenGL有所不一样,WebGL只容许一个颜色关联对象而OpenGL容许多个。
使用gl.framebufferRenderbuffer()函数将渲染缓冲区对象关联到帧缓冲区的深度关联对象:
function initFramebufferObject(gl) { //... gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer); //关联深度 //... }
其函数定义以下:
配置帧缓冲区的过程很复杂,WebGL提供了检查函数gl.checkFramebufferStatus():
function initFramebufferObject(gl) { //... // 检查帧缓冲区是否被正确设置 var e = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if (gl.FRAMEBUFFER_COMPLETE !== e) { console.log('Frame buffer object is incomplete: ' + e.toString()); return error(); } //... }
在须要在帧缓冲区绘制的时候调用绑定帧缓冲区对象,在须要在颜色缓冲区绘制的时候接触绑定。能够经过gl.bindFramebuffer()函数实现,具体可看下一节内容。
初始化准备工做完成后,接下来在加载数据的后进行图形绘制操做,调用绘制函数DrawDEM():
demFile.addEventListener("change", function (event) { //... reader.onload = function () { if (reader.result) { //读取 var terrain = new Terrain(); if (!readDEMFile(reader.result, terrain)) { console.log("文件格式有误,不能读取该文件!"); } //绘制 DrawDEM(gl, canvas, fbo, frameProgram, drawProgram, terrain); } }
readDEMFile()是读取解析DEM文件的函数,并保存到自定义的Terrain对象中,经过这个Terrain对象,调用DrawDEM()进行绘制:
//绘制 function DrawDEM(gl, canvas, fbo, frameProgram, drawProgram, terrain) { // 设置顶点位置 var demBufferObject = initVertexBuffersForDrawDEM(gl, terrain); if (!demBufferObject) { console.log('Failed to set the positions of the vertices'); return; } //获取光线:平行光 var lightDirection = getLight(); //预先给着色器传递一些不变的量 { //使用帧缓冲区着色器 gl.useProgram(frameProgram); //设置MVP矩阵 setMVPMatrix(gl, canvas, terrain.sphere, lightDirection, frameProgram); //使用颜色缓冲区着色器 gl.useProgram(drawProgram); //设置MVP矩阵 setMVPMatrix(gl, canvas, terrain.sphere, lightDirection, drawProgram); //将绘制在帧缓冲区的纹理传递给颜色缓冲区着色器的0号纹理单元 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, fbo.texture); gl.uniform1i(drawProgram.u_Sampler, 0); gl.useProgram(null); } //开始绘制 var tick = function () { //帧缓存绘制 gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); //将绘制目标切换为帧缓冲区对象FBO gl.viewport(0, 0, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT); // 为FBO设置一个视口 gl.clearColor(0.2, 0.2, 0.4, 1.0); // Set clear color (the color is slightly changed) gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Clear FBO gl.useProgram(frameProgram); //准备生成纹理贴图 //分配缓冲区对象并开启链接 initAttributeVariable(gl, frameProgram.a_Position, demBufferObject.vertexBuffer); // 顶点坐标 initAttributeVariable(gl, frameProgram.a_Color, demBufferObject.colorBuffer); // 颜色 //分配索引并绘制 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, demBufferObject.indexBuffer); gl.drawElements(gl.TRIANGLES, demBufferObject.numIndices, demBufferObject.indexBuffer.type, 0); //颜色缓存绘制 gl.bindFramebuffer(gl.FRAMEBUFFER, null); //将绘制目标切换为颜色缓冲区 gl.viewport(0, 0, canvas.width, canvas.height); // 设置视口为当前画布的大小 gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Clear the color buffer gl.useProgram(drawProgram); // 准备进行绘制 //分配缓冲区对象并开启链接 initAttributeVariable(gl, drawProgram.a_Position, demBufferObject.vertexBuffer); // Vertex coordinat //分配索引并绘制 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, demBufferObject.indexBuffer); gl.drawElements(gl.TRIANGLES, demBufferObject.numIndices, demBufferObject.indexBuffer.type, 0); window.requestAnimationFrame(tick, canvas); }; tick(); }
首先第一步仍然是初始化顶点缓冲区数组,可是与以前不一样的是这个只传输顶点数据到顶点缓冲区,并不链接顶点着色器,由于两组着色器是公用顶点数据的,因此须要在切换着色器的时候分配着色器并链接:
function initVertexBuffersForDrawDEM(gl, terrain) { //DEM的一个网格是由两个三角形组成的 // 0------1 1 // | | // | | // col col------col+1 var col = terrain.col; var row = terrain.row; var indices = new Uint16Array((row - 1) * (col - 1) * 6); var ci = 0; for (var yi = 0; yi < row - 1; yi++) { //for (var yi = 0; yi < 10; yi++) { for (var xi = 0; xi < col - 1; xi++) { indices[ci * 6] = yi * col + xi; indices[ci * 6 + 1] = (yi + 1) * col + xi; indices[ci * 6 + 2] = yi * col + xi + 1; indices[ci * 6 + 3] = (yi + 1) * col + xi; indices[ci * 6 + 4] = (yi + 1) * col + xi + 1; indices[ci * 6 + 5] = yi * col + xi + 1; ci++; } } var dem = new Object(); // Create the "Object" object to return multiple objects. // Write vertex information to buffer object dem.vertexBuffer = initArrayBufferForLaterUse(gl, terrain.vertices, 3, gl.FLOAT); dem.colorBuffer = initArrayBufferForLaterUse(gl, terrain.colors, 3, gl.FLOAT); dem.normalBuffer = initArrayBufferForLaterUse(gl, terrain.normals, 3, gl.FLOAT); dem.indexBuffer = initElementArrayBufferForLaterUse(gl, indices, gl.UNSIGNED_SHORT); if (!dem.vertexBuffer || !dem.colorBuffer || !dem.indexBuffer || !dem.normalBuffer) { return null; } dem.numIndices = indices.length; // Unbind the buffer object gl.bindBuffer(gl.ARRAY_BUFFER, null); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); return dem; }
为了知足交互需求,绘制函数仍然是经过刷新页面函数requestAnimationFrame()实现的,有的数据是固定随帧不变的,这样的数据能够提早传输好。固然,这些数据不包含共用的顶点缓冲区数据:
//获取光线:平行光 var lightDirection = getLight(); //预先给着色器传递一些不变的量 { //使用帧缓冲区着色器 gl.useProgram(frameProgram); //设置MVP矩阵 setMVPMatrix(gl, canvas, terrain.sphere, lightDirection, frameProgram); //使用颜色缓冲区着色器 gl.useProgram(drawProgram); //设置MVP矩阵 setMVPMatrix(gl, canvas, terrain.sphere, lightDirection, drawProgram); //将绘制在帧缓冲区的纹理传递给颜色缓冲区着色器的0号纹理单元 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, fbo.texture); gl.uniform1i(drawProgram.u_Sampler, 0); gl.useProgram(null); }
注意这里经过函数gl.useProgram()切换了着色器,而后再分别给着色器传输数据。在这个例子只是经过帧缓冲区作颜色中转,因此帧缓冲区和颜色缓冲区绘制的MVP矩阵是相同且固定的,因此能够提早传输好。而且,将帧缓冲区关联着颜色关联对象的纹理对象,分配给颜色缓冲区的片元着色器。
刷新页面函数requestAnimationFrame()的回调函数tick()中进行绘制,页面每隔一段时间就会调用这个绘制函数。
为了声明当前是绘制到帧缓存的,首先将要绑定帧缓冲区对象gl.bindFramebuffer()。而后调用gl.viewport()函数定义一个绘图的视口:
接下来仍是经过gl.useProgram()切换到对应的着色器,分配并链接顶点缓冲区的顶点数据;最后调用gl.drawElements()进行绘制便可。
相关的代码以下:
//开始绘制 var tick = function () { //帧缓存绘制 gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); //将绘制目标切换为帧缓冲区对象FBO gl.viewport(0, 0, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT); // 为FBO设置一个视口 gl.clearColor(0.2, 0.2, 0.4, 1.0); // Set clear color (the color is slightly changed) gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Clear FBO gl.useProgram(frameProgram); //准备生成纹理贴图 //分配缓冲区对象并开启链接 initAttributeVariable(gl, frameProgram.a_Position, demBufferObject.vertexBuffer); // 顶点坐标 initAttributeVariable(gl, frameProgram.a_Color, demBufferObject.colorBuffer); // 颜色 //分配索引并绘制 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, demBufferObject.indexBuffer); gl.drawElements(gl.TRIANGLES, demBufferObject.numIndices, demBufferObject.indexBuffer.type, 0); //... window.requestAnimationFrame(tick, canvas); }; tick(); }
绘制到颜色缓冲区的步骤也是一致的,只不过在绘制以前须要调用gl.bindFramebuffer(gl.FRAMEBUFFER, null)解除帧缓冲区绑定,将绘制目标切换到当前的颜色缓冲区。固然,设置视口和切换着色器操做都是必须的。相关代码以下:
//开始绘制 var tick = function () { //... //颜色缓存绘制 gl.bindFramebuffer(gl.FRAMEBUFFER, null); //将绘制目标切换为颜色缓冲区 gl.viewport(0, 0, canvas.width, canvas.height); // 设置视口为当前画布的大小 gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Clear the color buffer gl.useProgram(drawProgram); // 准备进行绘制 //分配缓冲区对象并开启链接 initAttributeVariable(gl, drawProgram.a_Position, demBufferObject.vertexBuffer); // Vertex coordinat //分配索引并绘制 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, demBufferObject.indexBuffer); gl.drawElements(gl.TRIANGLES, demBufferObject.numIndices, demBufferObject.indexBuffer.type, 0); window.requestAnimationFrame(tick, canvas); }; tick(); }
最后运行的结果以下,显示的是一个特定角度的地形:
跟以前教程相比,示例彷佛没有特别的地方。这个示例的关键点在于这个渲染效果通过了帧缓冲区的中转,给更深刻的技术作准备——好比,下一篇要论述的技术:阴影。
原本部分代码和插图来自《WebGL编程指南》,源代码连接:地址 。会在此共享目录中持续更新后续的内容。