利用FBO传送粒子数据

前言

直接用webGL画出成千上万的粒子,并不像在canvas中造出相应数量的粒子“实例”,然后统一绘制那么简单;因为直接在webGL中绘制时,实际上是用着色器去绘制,而着色器的API有限,最重要的是数据传入着色器内的方式有限;但是另一方面由于着色器内可以进行并行运算,因此如果将原本在CPU中进行的计算转移到GPU内就会大幅提高运行效率,画面也就更流畅了。

帧缓冲就可以解决大量数据传入的问题,不过用帧缓冲来传递数据本质上是利用帧缓冲作为数据写入介质,将数据通过帧缓冲保存到纹理上,然后将纹理传入相应的着色器;帧缓冲实际上的作用其实更加强大

帧缓冲

帧缓冲(frame buffer)本质上就是一块内存,不过这块内存是可以直接被显示器使用然后进行显示的。

而对应到webGL,帧缓冲就是一种用来组织图像(帧)渲染所需的内存资源的数据结构;图像渲染所需的内存资源,指的就是颜色缓冲(coloc buffer)、深度缓冲(depth buffer)以及模板缓冲(stencil buffer)这三种缓冲。

不过这三种缓冲是作为附加对象attachment)添加到帧缓冲里面的(我的理解是作为帧缓冲对象的一个属性),也就是这三种缓冲可以自由搭配添加到里面;比如只需要颜色缓冲,或者开启深度测试后加入深度缓冲等等。

webGL中,绘制操作都是在一个默认的帧缓冲上面进行的,也只有这个帧缓冲才会被显示出来;而其他被创建的帧缓冲虽然也可以进行绘制等操作,但都是不可见的,也就是离屏(off-screen)的;所以可以利用创建的帧缓冲来进行离屏渲染等操作。

A framebuffer (frame buffer, or sometimes framestore) is a portion of random-access memory (RAM) containing a bitmap that drives a video display.[1]

In WebGL, a framebuffer is a data structure that organizes the memory resources that are needed to render an image.[2]

在帧缓冲上绘制纹理

基本流程

在帧缓冲上绘制的原理就是:只要着色器程序绑定了某个帧缓冲,那么接下来的绘制操作都是在这个帧缓冲上面进行的。因此大致步骤如下;

  1. 创建帧缓冲

    1
    let frameBuffer = gl.createFramebuffer()
  2. 创建纹理对象

    1
    let frameTexture = gl.createTexture()
  3. 激活纹理通道

    1
    gl.activeTexture(gl.TEXTURE0 + textureID) // 激活对应的纹理

    这一步对于着色器如果同时存在多个纹理传入时是必须的;

  4. 绑定纹理对象

    1
    gl.bindTexture(gl.TEXTURE_2D, frameTexture)
  5. 纹理数据初始化很重要!);

    1
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null) // 先初始化纹理数据

    如果不进行纹理数据的初始化,相当于纹理的尺寸格式完全不知道;那么帧缓冲中绘制的信息也就传入不了;而且绘制时会有以下警告

    20200207172757

  6. 设置纹理参数

    1
    2
    3
    4
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) // 指定纹理S轴方向大小适应方式
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) // 指定纹理T轴方向大小适应方式
    gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) // 指定纹理缩小取样算法
    gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) // 指定纹理放大取样算法

    大致就是设置纹理的取样算法裁剪等等相关参数,也可以不设置,使用默认参数;

  7. 绑定帧缓冲

    1
    gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer) // 使用帧缓冲
  8. 将纹理对象绑定到帧缓冲的颜色缓冲上;

    1
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, frameTexture, 0) // 将纹理绑定到帧缓冲中的颜色缓冲上

    void gl.framebufferTexture2D(target, attachment, textarget, texture, level);

    framebufferTexture2D()函数的第二个参数attachment就是指定帧缓冲的哪个附加对象,即缓冲,在webGL1中,只有三个选项[3]

    • COLOR_ATTACHMENT0:对应帧缓冲中的颜色缓冲
    • DEPTH_ATTACHMENT:对应帧缓冲中的深度缓冲
    • STENCIL_ATTACHMENT:对应帧缓冲中的模板缓冲
  9. 绘制纹理:一般纹理的绘制需要单独的着色器来进行,也就需要另建一个着色器程序,并用这个着色器程序开启帧缓冲,然后绘制;不过需要注意的是,同一上下文创建的不同着色器程序,其纹理通道是共用的(有待验证),因此分配通道编号的时候需要注意!

如何准确地将数据绘制到纹理

纹理一般保存的就是颜色缓冲数据,而颜色缓冲本质上就是图像的像素信息;每个像素点实际上就是一个vec4类型的数据,颜色值的范围又在[0, 1]之间,所以数据值如果超出这个范围,那么就需要对数据进行处理了。

由于一个像素点最多存放4个数字(维度),因此当一个粒子的信息超过4个数字时就需要将信息进行分割,同时纹理空间也要进行相应地分割;

不需要分割数据时

当一个粒子信息小于等于4个维度时,那么可以直接按像素点的空间位置,连续的进行存储数据;若有剩余维度,可以补01

  1. 对粒子数据进行遍历;
  2. 得到粒子数据在帧缓冲中的设备坐标位置(坐标范围为[-1, 1]),按顺序存入一个一维数组(空间顺序可以按照从左至右,从上至下);
  3. 得到粒子数据在纹理中的纹理坐标位置(坐标范围为[0, 1]),按顺序存入一个一维数组
  4. 处理粒子数据,按顺序存入一个一维数组

:为何存放至一维数组?因为一维数组可以写入ABOArray Buffer Object数组缓冲对象)中传入着色器内;

按照上述流程,可以得到三个一维数组;其中设备坐标数组和粒子数据数组用于在帧缓冲中逐像素点绘制纹理,得到纹理后传入需要的着色器程序;纹理坐标数组则用于在着色器内精准地获取对应粒子存放在纹理中的数据

特别需要注意的是:虽然帧缓冲中绘制的图像就是纹理图像,但是纹理空间设备空间的坐标系范围不一样!也就是说,同一个像素点在帧缓冲和在纹理中的坐标是不一样的!

20200207172825


虽然坐标系有点不同,但是按理直接进行坐标系变换不就一样可以将设备坐标转化为同一像素点对应的纹理坐标吗?我一开始确实也是这么做的,但是实际变换后发现着色器展现的信息好像跟我写入纹理的不太一样;我发现着色器展现的数据好像是我写入纹理当中的一半,我左思右想没想到原因;唯一的原因可能就是纹理坐标不是精确的像素点位置,导致取样的时候进行了插值!而且“原本数据的一半”不难让人想到是原像素点和一个空像素点(即vec4(0.0, 0.0, 0.0, 0.0))之间进行的插值,所以我将变换后得到的纹理坐标统一向右和向下偏移了0.5个像素位置,取样的数据竟然神奇的与我写入的一致了……

不过目前还不清楚是我写入帧缓冲的像素点位置有问题还是帧缓冲转纹理数据普遍具有这样的问题。


需要数据分割时

当粒子数据超过4个维度时,可以将数据拆分几个不同的4维数据(一般可以按照数据的功能进行拆分,如:速度,大小,颜色信息等等),分区域进行数据填充;实际上仍然可以共用一套索引坐标(即纹理坐标),然后按规律找出其他区域的索引,这样就能够将数据维度扩充到4维以上了!

只要确定好拆分的规则,以及解析其他区域索引的规则,剩下的就是一样的操作了。

这里有个案例可以参考:[WebGL] 百万流畅流体粒子 - 掘金

设置帧缓冲的尺寸

帧缓冲的默认尺寸实际上就是canvas的尺寸大小,但是为了节省内存和加快数据传输,可以将帧缓冲的尺寸调整成适应数据量长度的大小,以免造成过多的空间被闲置

可以使用viewport()方法来设置帧缓冲的窗口大小:

void gl.viewport(x, y, width, height);

  • x:原点的x坐标(原点的位置是区域的左下角);单位为像素(下面单位都一样)。
  • y:原点的y坐标;
  • width:窗口宽度;
  • height:窗口高度;

需要注意的是,在帧缓冲上绘制完纹理后,需要把窗口大小切回原来的尺寸(貌似窗口大小设置对所有的帧缓冲有效……)!

参考文档


  1. Framebuffer - Wikipedia ↩︎

  2. Introduction to Computer Graphics, Section 7.4 – Framebuffers ↩︎

  3. WebGLRenderingContext.framebufferTexture2D() - Web APIs | MDN ↩︎