JavaScript WebGL 幀緩衝區物件

XXHolic發表於2022-02-01

引子

在看 How I built a wind map with WebGL 的時候,裡面用到了 framebuffer ,就去查了下資料單獨嘗試了一下。

幀緩衝區物件

WebGL 有一個能力是將渲染結果作為紋理使用,使用到的就是幀緩衝區物件(framebuffer object)。

在預設情況下,WebGL 最終繪圖結果儲存在顏色緩衝區,幀緩衝區物件可以用來代替顏色緩衝區,如下圖所示,繪製在幀緩衝區中的物件並不會直接顯示在 Canvas 上,因此這種技術也被稱為離屏繪製(offscreen drawing)。

99-1

示例

為了驗證上面的功能,這個示例會在幀緩衝區裡面繪製一張圖片,然後將其作為紋理再次繪製顯示出來。

基於使用圖片示例的邏輯,主要有下面幾個方面的變化:

  • 資料
  • 幀緩衝區物件
  • 繪製

資料

在幀緩衝區裡面繪製跟正常的繪製一樣,只是不顯示,所以也要有對應的繪製區域大小、頂點座標和紋理座標。

  offscreenWidth: 200, // 離屏繪製的寬度
  offscreenHeight: 150, // 離屏繪製的高度
  // 部分程式碼省略
  // 針對幀緩衝區繪製的頂點和紋理座標
  this.offScreenBuffer = this.initBuffersForFramebuffer(gl);
  // 部分程式碼省略
  initBuffersForFramebuffer: function (gl) {
    const vertices = new Float32Array([
      0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, -0.5,
    ]); // 矩形
    const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
    const texCoords = new Float32Array([
      1.0,
      1.0, // 右上角
      0.0,
      1.0, // 左上角
      0.0,
      0.0, // 左下角
      1.0,
      0.0, // 右下角
    ]);

    const obj = {};
    obj.verticesBuffer = this.createBuffer(gl, gl.ARRAY_BUFFER, vertices);
    obj.indexBuffer = this.createBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, indices);
    obj.texCoordsBuffer = this.createBuffer(gl, gl.ARRAY_BUFFER, texCoords);

    return obj;
  },
  createBuffer: function (gl, type, data) {
    const buffer = gl.createBuffer();
    gl.bindBuffer(type, buffer);
    gl.bufferData(type, data, gl.STATIC_DRAW);
    gl.bindBuffer(type, null);
    return buffer;
  }
  // 部分程式碼省略

頂點著色器和片元著色器都可以新定義,這裡為了方便公用了一套。

幀緩衝區物件

想要在幀緩衝區繪製,需要建立對應的幀緩衝區物件。

  // 幀緩衝區物件
  this.framebufferObj = this.createFramebufferObject(gl);
  // 部分程式碼省略
  createFramebufferObject: function (gl) {
    let framebuffer = gl.createFramebuffer();

    let texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(
      gl.TEXTURE_2D,
      0,
      gl.RGBA,
      this.offscreenWidth,
      this.offscreenHeight,
      0,
      gl.RGBA,
      gl.UNSIGNED_BYTE,
      null
    );
    // 反轉圖片 Y 軸方向
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    // 紋理座標水平填充 s
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    // 紋理座標垂直填充 t
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    // 紋理放大處理
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    // 紋理縮小處理
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    framebuffer.texture = texture; // 儲存紋理物件

    // 關聯緩衝區物件
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
    gl.framebufferTexture2D(
      gl.FRAMEBUFFER,
      gl.COLOR_ATTACHMENT0,
      gl.TEXTURE_2D,
      texture,
      0
    );

    // 檢查配置是否正確
    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);

    return framebuffer;
  }
  // 部分程式碼省略
  • createFramebuffer 函式建立幀緩衝區物件,刪除物件的函式是 deleteFramebuffer
  • 建立好後,需要將幀緩衝區的顏色關聯物件指定一個紋理物件,示例建立的紋理物件有幾個特點:1 紋理的寬高跟繪製區域寬高一致;2 使用 texImage2D 時最後一個引數為 null ,也就是預留了一個空白的儲存紋理物件的區域;3 建立好的紋理物件放到了幀緩衝區物件上,就是這行程式碼 framebuffer.texture = texture
  • bindFramebuffer 函式將幀緩衝區繫結到目標上,然後使用 framebufferTexture2D 將前面建立的紋理物件繫結到幀緩衝區的顏色關聯物件 gl.COLOR_ATTACHMENT0 上。
  • checkFramebufferStatus 檢查幀緩衝區物件配置是否正確。

繪製

繪製時候主要的區別是有切換的過程:

// 部分程式碼省略
  draw: function () {
    const gl = this.gl;
    const frameBuffer = this.framebufferObj;
    this.canvasObj.clear();
    const program = this.shaderProgram;
    gl.useProgram(program.program);

    // 這個就讓繪製的目標變成了幀緩衝區
    gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
    gl.viewport(0, 0, this.offscreenWidth, this.offscreenHeight);
    this.drawOffFrame(program, this.imgTexture);

    // 解除幀緩衝區繫結,繪製的目標變成了顏色緩衝區
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    this.drawScreen(program, frameBuffer.texture);
  },
  // 部分程式碼省略
  • 先使用 bindFramebuffer 讓繪製的目標變成幀緩衝區,需要指定對應的視口。
  • 幀緩衝區繪製完成後解除繫結,恢復到正常預設的顏色緩衝區,同樣需要指定對應的視口,還要比較特別的是使用了緩衝區物件的紋理,這個表明就是從幀緩衝區得到的繪製結果。

觀察及思考

網上找的相關示例感覺比較複雜,嘗試簡化的過程中有下面的一些觀察和思考。

framebuffer.texture 是本來就有的屬性還是人為新增的 ?

在建立幀緩衝區物件的時候有這個邏輯: framebuffer.texture = texture ,那麼幀緩衝區物件本身就有 texture 屬性嗎?

列印日誌發現剛建立的時候並沒有這個屬性,所以推測應該是人為的新增。

framebuffer.texture 什麼時候有的內容 ?

初始化幀緩衝區物件的時候,儲存的紋理是空白的,但從最終結果來看,在幀緩衝區繪製之後,紋理就有內容了,那麼 framebuffer.texture 屬性是什麼時候有了內容?

在繪製邏輯中,跟紋理相關語句有:

  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.uniform1i(program.uSampler, 0);
  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

推測是 gl.drawElements 方法繪製結果儲存在幀緩衝區的顏色關聯物件,幀緩衝區的顏色關聯物件又在初始化時關聯了建立的空白紋理 物件,framebuffer.texture 指向的也是同一個空白紋理物件,所以最終就有了內容。

最終的顯示為什麼沒有鋪滿整個畫布?

最終繪製可顯示的內容時,可以發現頂點對應整個畫布,紋理座標對應的整個完整的紋理,但為什麼沒有鋪滿整個畫布?

最終繪製可顯示內容時使用的紋理來自幀緩衝區的繪製結果,而幀緩衝區的頂點對應的是整個緩衝區域的一半,如果把整個幀緩衝區繪製結果當做一個紋理,按照最終繪製可視區比例縮放,那麼最後的繪製沒有鋪滿就是預期正確的結果。

這個是鋪滿畫布的示例,只需將緩衝區頂點調整為對應整個緩衝區大小。

參考資料

相關文章