webgl——使用FBO实现离屏渲染

时间:2022-05-15 04:45:00

        在3D应用中我们经常会使用离屏渲染来进一步绘制场景,从而提高场景的视觉效果。离屏渲染就我的理解来说,首先需要将需要离屏渲染的物体,绘制到帧缓冲当中并将其当成一个纹理,在对其进行处理,处理完成后再将这个纹理贴到需要渲染的物体上。在OpenGLES当中使用FBO实现离屏渲染的案例已经有很多了,当我想将其移植到WebGL当中时却发现可供参考的案例十分的少,于是我花了几天时间研究,并在这里做以总结。

        为了能够清晰地说明问题,我选择了一个十分简单的WebGL案例并对其加入离屏渲染的部分。该案例原来是绘制一个绿色的三角形并且沿着其Y轴旋转,改造之后它的纹理变为一个小三角形沿着Z轴与大三角形一起旋转。

        下面先说说实现它的整体思路:

        1.创建FBO对象(FrameBufferObject),也就是我们说的帧缓冲对象,FBO在使用时必须对其进行绑定,如果绑定参数为NULL则说明绑定系统默认帧缓冲,即直接绘制到屏幕上。

        2.创建渲染缓存对象(RenderBufferObject),开始我对这部分很不理解,觉得每次开辟了FBO之后,就可以直接完成帧缓冲的绘制,FBO存储帧缓冲,Texture对象保存纹理数据,那么渲染缓存对象是做什么用的呢。后来了解到,并不是所有的数据都能够被纹理格式所储存的,比如深度缓存与模板缓存的数据就需要使用它来存放,本案例中,就是使用渲染缓冲对象来存储深度数据的(虽然也没有用到)。

        3.创建纹理对象(Texture),这一步比较简单,就是将绘制完成的帧缓冲对象以纹理的形式存放,以供下次使用。

        给我的感觉就是,FBO就像一个画板,纹理和RenderBufferObject就像画笔和纸张,在画板之中,画笔和纸张的排列组合最终构成了一幅幅美丽的画卷~

        介绍完了整体思路,下面介绍离屏渲染的具体开发过程:

function initFramebufferObject(gl) {
     framebuffer = gl.createFramebuffer();

    // 新建纹理对象作为帧缓冲区的颜色缓冲区对象
     texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 256, 256, 0, gl.RGB, gl.UNSIGNED_BYTE, null);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    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);

    // 新建渲染缓冲区对象作为帧缓冲区的深度缓冲区对象
    var depthBuffer = gl.createRenderbuffer();
    gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
    gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, 256, 256);

    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
    gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);

    // 检测帧缓冲区对象的配置状态是否成功
    var e = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
    if (gl.FRAMEBUFFER_COMPLETE !== e) {
        console.log('Frame buffer object is incomplete: ' + e.toString());
        return;
    }

    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.bindTexture(gl.TEXTURE_2D, null);
    gl.bindRenderbuffer(gl.RENDERBUFFER, null);
}
        上述代码创建了FBO对象、RenderBuffer对象和Texture对象,并设置好了他们的参数,将他们进行绑定,最后检测了FBO对象配置状态是否成功,这里需要注意的是,我们开辟内存空间都是256,如果这个值更大,最后绘制出来图像会更加精细,反之则更粗糙。

        还有一点更为重要,将绑定好的纹理和渲染缓冲区,帧缓冲区都取消绑定,这是一个良好的习惯,使用时绑定,不需要使用时取消绑定,为什么这么强调这一点,因为。。。我花了很长时间卡到了这里。。。所以提醒大家一定要这么做!

this.drawSelf=function(ms, tex)//绘制物体的方法
{  
   gl.useProgram(this.program);//指定使用某套着色器程序
   //获取总变换矩阵引用id
   var uMVPMatrixHandle=gl.getUniformLocation(this.program, "uMVPMatrix");
   //将总变换矩阵送入渲染管线
   gl.uniformMatrix4fv(uMVPMatrixHandle,false,new Float32Array(ms.getFinalMatrix()));
   
   gl.enableVertexAttribArray(gl.getAttribLocation(this.program, "aPosition"));//启用顶点坐标数据数组
   gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); //绑定顶点坐标数据缓冲
   //给管线指定顶点坐标数据
   gl.vertexAttribPointer(gl.getAttribLocation(this.program,"aPosition"),3,gl.FLOAT,false,0, 0);

       //启用纹理坐标数据
       gl.enableVertexAttribArray(gl.getAttribLocation(this.program, "aTexCoor"));
       //将顶点纹理坐标数据送入渲染管线
       gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexTexCoorBuffer);
       gl.vertexAttribPointer(gl.getAttribLocation(this.program, "aTexCoor"), 2, gl.FLOAT, false, 0, 0);         //一定要每次都取消绑定,在这里卡了很久

       var isImg = gl.getUniformLocation(this.program, 'isImg');
       if(tex){
           // gl.activeTexture(gl.TEXTURE0);
           gl.uniform1i(isImg, true);
   }else{
           gl.uniform1i(isImg, false);
   }

       //一定要每次都取消绑定,在这里卡了很久
       gl.drawArrays(gl.TRIANGLES, 0, this.vcount);       //用顶点法绘制物体
       gl.bindFramebuffer(gl.FRAMEBUFFER, null);
       gl.bindTexture(gl.TEXTURE_2D, null);
}
        这是三角形的绘制代码,在每一帧中需要绘制两个三角形,第一个三角形是绿色的绘制到帧缓冲当中,第二次绘制使用第一次绘制的图案作为纹理。所以这里设置了两个标志位当其是第一次绘制时在着色器中使用绿色,第二次则使用传递的纹理。由于每次绘制前都开启了这次绘制需要使用的FrameBuffer和Texture所以每次绘制完成也要关闭它,嗯,很重要。

  //绘制一帧画面的方法
  function drawFrame()
  {      
if(!ooTri){
   alert("加载未完成!");//提示信息
   return;
}
         // 在帧缓冲区的颜色关联对象即纹理对象中绘制立方体,纹理使用图片
         gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);// 绑定帧缓冲区对象后绘制就会在绑定帧缓冲区中进行绘制

         gl.viewport(0, 0, 256, 256);
         gl.clearColor(0.2, 0.2, 0.4, 1.0);
         gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
         //保护现场
         ms.pushMatrix();
         //执行旋转
         ms.rotate(currentAngle,0,0,1);

         ooTri.drawSelf(ms, false);
         //恢复现场
         ms.popMatrix();

         // 在canvas上绘制矩形,纹理使用上一步在纹理对象中绘制的图像
         gl.bindFramebuffer(gl.FRAMEBUFFER, null);// 接触绑定之后,会在默认的颜色缓冲区中绘制
         gl.bindTexture(gl.TEXTURE_2D, texture);

         gl.viewport(0, 0, 800, 800);

         //背景颜色_黑色
         gl.clearColor(0.0, 0.0, 0.0, 1.0);

         //清除着色缓冲与深度缓冲
         gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);


      //保护现场
      ms.pushMatrix(); 
      //执行旋转
ms.rotate(currentAngle,0,1,0);
//绘制物体
ooTri.drawSelf(ms, true);
//恢复现场
ms.popMatrix();
//修改旋转角度
      currentAngle += incAngle;
      if (currentAngle > 360)//保证角度范围不超过360
          currentAngle -= 360;           
  }
        最后是绘制一帧的方法,首先绑定好FBO设置视口的大小,清空颜色和深度值(两次清空背景色的颜色不同,不然看不到大三角形),然后绘制沿着Z轴旋转的三角形,然后在绘制沿着Y轴旋转的三角形。

        下面是效果图片:

webgl——使用FBO实现离屏渲染webgl——使用FBO实现离屏渲染

最后放github地址:https://github.com/StringKun/WebGL-FBO