webgl 纹理进阶(三)---------纹理与FBO

355 阅读2分钟

一.简介

本文主要记录纹理与FBO相关的用法,第一个是将FBO渲染到纹理(RTT),可以借此实现倒影,小地图等效果,第二种是借助FBO实现GPU选择

除此之外,纹理和FBO结合可以实现各种后期处理效果,有机会慢慢记录

二.渲染到纹理(RTT)

2.1 创建一个纹理,用于FBO的颜色关联对象

/**
     * 使用图片创建纹理
     * */
    function createTexture(image, width, height) {
      const texture = gl.createTexture();
      gl.bindTexture(gl.TEXTURE_2D, texture);
      gl.activeTexture(gl.TEXTURE0);
      gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);

      if (image) {
        //给纹理赋值图片
        gl.texImage2D(
          gl.TEXTURE_2D,
          0,
          gl.RGB,
          gl.RGB,
          gl.UNSIGNED_BYTE,
          image
        );
      } else {
          //若无图片,创建一个空纹理,用于FBO写入
        gl.texImage2D(
          gl.TEXTURE_2D,
          0,
          gl.RGB,
          width,
          height,
          0,
          gl.RGB,
          gl.UNSIGNED_BYTE,
          null
        );
      }

      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);
      return texture;
    }

在创建FBO纹理时,只需要将image传为null即可

2.2 创建一个FBO,关联深度渲染缓冲区和颜色纹理

 /**
     * 创建帧缓冲区
     * */
    function createFBO(width, height) {
      const fbo = gl.createFramebuffer();
      gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

      //创建颜色附件
      const texture = createTexture(null, width, height);

      //将颜色附件绑定到FBO上
      gl.framebufferTexture2D(
        gl.FRAMEBUFFER,
        gl.COLOR_ATTACHMENT0,
        gl.TEXTURE_2D,
        texture,
        0
      );
      //把数据写入纹理
      fbo.texture = texture;

      //创建渲染缓冲区对象并设置其尺寸和参数
      const depthBuffer = gl.createRenderbuffer();
      if (!depthBuffer) {
        console.log("无法创建渲染缓冲区对象");
        return error();
      }

      gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
      gl.renderbufferStorage(
        gl.RENDERBUFFER,
        gl.DEPTH_COMPONENT16,
        width,
        height
      );
      gl.framebufferRenderbuffer(
        gl.FRAMEBUFFER,
        gl.DEPTH_ATTACHMENT,
        gl.RENDERBUFFER,
        depthBuffer
      );
      const result = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
      if (gl.FRAMEBUFFER_COMPLETE !== result) {
        throw new Error("帧缓冲区绑定有误");
      }
      gl.bindFramebuffer(gl.FRAMEBUFFER,null)
      return fbo;
    }

2.3 创建box,绘制到FBO

 function drowObj(
   obj,
   program,
   fbo,
   viewProjectMatrix,
   currentAngle,
   texture
 ) {
   gl.useProgram(program.program);

   if (fbo) {
     //绘制到FBO上
     gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
     //清空FBO颜色和深度缓冲区
     gl.clearColor(1, 1, 1, 1);
     gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
   }

   //绑定box vao
   vaoExt.bindVertexArrayOES(obj.vao);
   //使用viewPorject矩阵
   const viewProjectLoc = program.uniformLocs.u_viewProjectMatrixLoc;
   gl.uniformMatrix4fv(viewProjectLoc, false, viewProjectMatrix.elements);

   obj.modelMatrix.setRotate(currentAngle, 0, 1, 0);
   const modelMatrixLoc = program.uniformLocs.u_modelMatrixLoc;
   gl.uniformMatrix4fv(modelMatrixLoc, false, obj.modelMatrix.elements);

   if(texture){
     const textureLoc = program.uniformLocs.u_textureLoc;
     gl.bindTexture(gl.TEXTURE_2D, texture);
     gl.uniform1i(textureLoc, 0);
   }else{
     //设置batchId
     const rgb=encodeToRGB(plane.id)
     console.log(rgb)
     gl.uniform3fv(program.uniformLocs.u_batchIdLoc, rgb)
   }

   gl.drawElements(gl.TRIANGLES, obj.length, gl.UNSIGNED_BYTE, 0);

   gl.bindFramebuffer(gl.FRAMEBUFFER, null);
   gl.bindTexture(gl.TEXTURE_2D, null);
 }

2.4 再次绘制plane到屏幕上,并将FBO的颜色附件作为纹理

gl.clearColor(0, 0, 0, 1);
 function draw() {
   currentAngle += 0.5;

  
   
   //设置相机参数
   viewProjectMatrix.set(camera.projectMatrix);
   viewProjectMatrix.multiply(camera.viewMatrix);

   gl.enable(gl.DEPTH_TEST)

   //把fbo的纹理赋值给plane
  
   // gl.bindTexture(gl.TEXTURE_2D,fboTexture)
   ////////////////////////////画中画/////////////////////////
      drowObj(
       box,
       textureProgram,
       fbo,
       viewProjectMatrix,
       currentAngle,
       boxTexture,
     );
     const fboTexture=fbo.texture
     gl.clearColor(0, 0, 0, 1);
     gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
     drowObj(
       plane,
       textureProgram,
       null,
       viewProjectMatrix,
       currentAngle,
       fboTexture,
     );
 }

效果如下:

image.png

二.GPU选择

使用GPU选择原理如下:

  • 首先给每个object赋值一个id,并将id编码为RGB值
  • 绑定FBO,绘制物体,物体的颜色使用编码出来的RGB值
  • 解绑缓冲区,按照物体的实际颜色绘制
  • 点击时,绑定FBO,使用gl.readpixel读取颜色值,将颜色解码为id,遍历,寻找物体

2.1. 给出一个简单编码,将id编码为RGB,和将RGB解码为id

    function encodeToRGB(id){
      const color=[]
      let i=2
      while(i>0){
        const delta=Math.pow(255,i)
        
        color.push(Math.floor(id/delta))
        id=id%delta
        --i
      }
      color.push(id)
      return color
    }


    function decodeRGBA(color){
      let value=0
      for(let i=1;i<=3;i++){
        const delta=Math.pow(255,3-i)
        value+=color[i-1]*delta
      }
      return value
    }

2.2 使用颜色值绘制到FBO,然后解绑FBO,使用实际颜色绘制

  const vs = /*glsl*/ `
      attribute vec3 a_position;
      attribute vec2 a_uv;
      uniform mat4 u_viewProjectMatrix;
      uniform mat4 u_modelMatrix;
      uniform int batchId;
      varying vec2 v_uv;
      void main(){
        gl_Position=u_viewProjectMatrix*u_modelMatrix*vec4(a_position,1.0);
        v_uv=a_uv;
      }
    `;

    const fs = /*glsl*/ `
      precision mediump float;
      uniform sampler2D texture;
      varying vec2 v_uv;
      void main(){
        vec4 color=texture2D(texture,v_uv);
        gl_FragColor=color;
      }
    
    `;

    const colorFS = /*glsl*/ `
     precision mediump float;
      uniform vec3 u_batchId;
      void main(){
        gl_FragColor=vec4(u_batchId/255.0,1.0);
      }
    `;
    const textureProgram = createShaderProgram(vs, fs);
    const colorProgram=createShaderProgram(vs,colorFS)
    const box = initBox(textureProgram.attributeLocs, vaoExt);
    box.id=1000000
    const plane = initPlane(textureProgram.attributeLocs, vaoExt);
    plane.id=2000000
    function draw() {
      currentAngle += 0.5;

     
      
      //设置相机参数
      viewProjectMatrix.set(camera.projectMatrix);
      viewProjectMatrix.multiply(camera.viewMatrix);

      gl.enable(gl.DEPTH_TEST)
    
      if (ispick) {
        drowObj(
          plane,
          colorProgram,
          fbo,
          viewProjectMatrix,
          currentAngle,
          null,
        );
      } 
      drowObj(
          plane,
          textureProgram,
          null,
          viewProjectMatrix,
          currentAngle,
          boxTexture,
        );
      
    }
    
    function createShaderProgram(vs, fs) {
      const program = createProgram(gl, vs, fs);
      //获取位置
      const a_positionLoc = gl.getAttribLocation(program, "a_position");
      const a_uvLoc = gl.getAttribLocation(program, "a_uv");
      const u_viewProjectMatrixLoc = gl.getUniformLocation(
        program,
        "u_viewProjectMatrix"
      );
      const u_modelMatrixLoc = gl.getUniformLocation(program, "u_modelMatrix");
      const u_textureLoc = gl.getUniformLocation(program, "texture");
      const u_batchIdLoc = gl.getUniformLocation(program, "u_batchId");
      return {
        program,
        attributeLocs: {
          a_positionLoc,
          a_uvLoc,
        },
        uniformLocs: {
          u_viewProjectMatrixLoc,
          u_modelMatrixLoc,
          u_textureLoc,
          u_batchIdLoc,
        },
      };
    }
    

2.3 点击时,绑定FBO,读取颜色并解码,之后遍历场景中的物体,即可知道选择了哪个物体

 document.getElementById("canvas").onclick = (e) => {
    const x = e.offsetX;
    const y = e.offsetY;
    ispick=true
    draw();
    gl.bindFramebuffer(gl.FRAMEBUFFER,fbo)
    const pixels = new Uint8Array(4);
    gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
    gl.bindFramebuffer(gl.FRAMEBUFFER,null)
    ispick = false;
    alert('id是'+decodeRGBA(pixels))
  };

image.png