风场可视化:A GPU Approach to Particle Physics


引子
  • 正文
  • 粒子状态编码为颜色
  • 状态保持
  • 纹理作为顶点属性缓冲区
  • 障碍物
  • 未来想法
  • 参考资料
  • 原文中有提到参考的教程,就去看了下,发现对一些逻辑的理解很有帮助,顺便翻译记录一下。

    • 原文:A GPU Approach to Particle Physics

    • Origin

    • My GitHub

    GPGPU 系列的下一个项目是一个粒子物理引擎,它在 GPU 上计算整个物理模拟。粒子受重力影响,会与场景几何体产生反弹。这个 WebGL 演示使用了着色器功能,并不需要严格按照 OpenGL ES 2.0 规范要求,因此它可能在某些平台上无法工作,尤其是在移动设备上。这将在本文后面讨论。

    • https://skeeto.github.io/webgl-particles/ (source)

    它是可交互的。鼠标光标是一个让粒子反弹的圆形障碍物,单击将在模拟中放置永久性障碍物。你可以绘制粒子可以流过的结构。

    这是示例的 HTML5 视频展示,出于必要,它以每秒 60 帧的高比特率录制,所以它相当大。视频编解码器不能很好地处理全屏所有粒子,较低的帧率也不能很好地捕捉效果。我还添加了一些在实际演示中听不到的声音。

    • 视频播放地址:https://nullprogram.s3.amazonaws.com/particles/particles.mp4

    在现代 GPU 上,它可以以每秒 60 帧的速度模拟并且绘制超过 4 百万个粒子。请记住,这是一个 JavaScript 应用程序,我没有真正花时间优化着色器,它受 WebGL 的约束,而不是像 OpenCL 或至少桌面 OpenGL 这样更适合一般计算的东西。

    Game of Life 和 path finding 项目一样,模拟状态存储在成对的纹理中,大部分工作是在片元着色器中通过它们之间逐像素映射完成。我不会重复这个设置细节,所以如果你需要了解它是如何工作的,请参考 Game of Life 一文。

    对于这个模拟,这些纹理中有四个而不是两个:一对位置纹理和一对速度纹理。为什么是成对的纹理?有 4 个通道,因此其中的每一个部分(x、y、dx、dy)都可以打包到自己的颜色通道中。这似乎是最简单的解决方案。

    101-1

    这个方案的问题是缺乏精确性。对于 R8G8B8A8 内部纹理格式,每个通道为一个字节。总共有 256 个可能的值。显示区域为 800×600 像素,因此显示区域上的每个位置不能都显示。幸运的是,两个字节(总计 65536 个值)对于我们来说已经足够了。

    101-2
    101-3

    下一个问题是如何跨这两个通道编码值。它需要覆盖负值(负速度),并应尽量充分利用动态范围,比如尝试使用所有 65536 个范围内的值。

    要对一个值编码,将该值乘以一个标量,将其扩展到编码的动态范围。选择标量时,所需的最高值(显示的尺寸)是编码的最高值。

    接下来,将动态范围的一半添加到缩放值。这会将所有负值转换为正值,0 表示最小值。这种表示法称为 Excess-K 。其缺点是用透明黑色清除纹理(glClearColor)不能将解码值设置为 0 。

    最后,将每个通道视为基数为 256 的数字。OpenGL ES 2.0 着色器语言没有按位运算符,因此这是使用普通的除法和模来完成的。我用 JavaScript 和 GLSL 制作了一个编码器和解码器。JavaScript 需要它来写入初始值,并且出于调试目的,它可以读回粒子位置。

    vec2 encode(float value) {
        value = value * scale + OFFSET;
        float x = mod(value, BASE);
        float y = floor(value / BASE);
        return vec2(x, y) / BASE;
    }
    
    float decode(vec2 channels) {
        return (dot(channels, vec2(BASE, BASE * BASE)) - OFFSET) / scale;
    }
    

    JavaScript 与上面的标准化 GLSL 值(0.0-1.0)不同,这会生成一个字节的整数(0-255),用于打包到类型化数组中。

    function encode(value, scale) {
        var b = Particles.BASE;
        value = value * scale + b * b / 2;
        var pair = [
            Math.floor((value % b) / b * 255),
            Math.floor(Math.floor(value / b) / b * 255)
        ];
        return pair;
    }
    
    function decode(pair, scale) {
        var b = Particles.BASE;
        return (((pair[0] / 255) * b +
                 (pair[1] / 255) * b * b) - b * b / 2) / scale;
    }
    

    更新每个粒子的片元着色器在该粒子的“索引”处对位置和速度纹理进行采样,解码它们的值,对它们进行操作,然后将它们编码回一种颜色,以便写入输出纹理。因为我使用的是 WebGL ,它缺少多个渲染目标(尽管支持 gl_FragData ),所以片元着色器只能输出一种颜色。位置在一个过程中更新,速度在另一个过程中更新为两个单独的绘图。缓冲区在两个过程完成才会交换,因此速度着色器(有意)不会使用更新的位置值。

    最大纹理大小有一个限制,通常为 8192 或 4096 ,因此纹理不是以一维纹理排列,而是保持方形。粒子由二维坐标索引。

    看到直接绘制到屏幕上而不是正常显示的位置或速度纹理非常有趣。这是观看模拟的另一个领域,它甚至帮助我发现了一些其它方面很难看到的问题。输出是一组闪烁的颜色,但有明确的模式,展示了系统的许多状态(或不在其中的状态)。我想分享一段视频,但编码比普通显示更不切实际。以下是截图:位置,然后是速度。这里没有捕捉到阿尔法分量。

    101-4
    101-5

    OpenGL ES 着色器语言规范(PDF)时,我产生了这个项目的想法。我一直想做一个粒子系统,但我卡在如何绘制粒子的问题上。表示位置的纹理数据需要以某种方式作为顶点反馈到管道中。通常,缓冲区纹理——由数组缓冲区支持的纹理——或像素缓冲区对象——异步纹理数据复制——可用于此操作,但 WebGL 没有这些功能。从 GPU 中提取纹理数据,并将其作为每帧上的数组缓冲区重新加载是不可能的。

    然而,我想出了一个很酷的技巧,比这两个都好。着色器函数 texture2D 用于对纹理中的像素进行采样。通常情况下,片元着色器将其用作计算一个像素颜色过程的一部分。但是着色器语言规范提到,texture2D 也可以在顶点着色器中使用。就在那时,一个点子击中了我。顶点着色器本身可以执行从纹理到顶点的转换

    它的工作原理是将前面提到的二维粒子索引作为顶点属性传递,使用它们从顶点着色器中查找粒子位置。着色器将以 GL_POINTS 模式运行,发射点粒子。这是简略的版本:

    attribute vec2 index;
    
    uniform sampler2D positions;
    uniform vec2 statesize;
    uniform vec2 worldsize;
    uniform float size;
    
    // float decode(vec2) { ...
    
    void main() {
        vec4 psample = texture2D(positions, index / statesize);
        vec2 p = vec2(decode(psample.rg), decode(psample.ba));
        gl_Position = vec4(p / worldsize * 2.0 - 1.0, 0, 1);
        gl_PointSize = size;
    }
    

    真实版本也会对速度进行采样,因为它会调节颜色(缓慢移动的粒子比快速移动的粒子更亮)。

    然而,有一个潜在的问题:允许实现将顶点着色器纹理绑定的数量限制为 0(GL_MAX_vertex_texture_IMAGE_UNITS)。所以从技术上讲,顶点着色器必须始终支持 texture2D ,但它们不需要支持实际的纹理。这有点像飞机上不载客的餐饮服务。有些平台不支持这种技术。到目前为止,我只在一些移动设备上遇到过这个问题。

    除了缺乏一些平台的支持之外,这允许模拟的每个部分都留在 GPU 上,并为纯 GPU 粒子系统铺平道路。

    liquid demo 可以像这样在 GPU 上运行。如果我猜想正确,粒子会增大体积,形成碗状的障碍物会填满,而不是将粒子集中到一个点上。

    我认为这个项目还有一些需要探索的地方。

    Back to top

    A GPU Approach to Particle Physics