3

JavaScript WebGL 帧缓冲区对象

 2 years ago
source link: https://segmentfault.com/a/1190000041361406
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在看 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 指向的也是同一个空白纹理对象,所以最终就有了内容。

最终的显示为什么没有铺满整个画布?

最终绘制可显示的内容时,可以发现顶点对应整个画布,纹理坐标对应的整个完整的纹理,但为什么没有铺满整个画布?

最终绘制可显示内容时使用的纹理来自帧缓冲区的绘制结果,而帧缓冲区的顶点对应的是整个缓冲区域的一半,如果把整个帧缓冲区绘制结果当做一个纹理,按照最终绘制可视区比例缩放,那么最后的绘制没有铺满就是预期正确的结果。

这个是铺满画布的示例,只需将缓冲区顶点调整为对应整个缓冲区大小。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK