共计 4802 个字符,预计需要花费 13 分钟才能阅读完成。
引子
想到原文中有提到参考的教程,就去看了下,发现对一些逻辑的了解很有帮忙,顺便翻译记录一下。
- 原文:A GPU Approach to Particle Physics
- Origin
- My GitHub
注释
我的 GPGPU 系列的下一个我的项目是一个粒子物理引擎,它在 GPU 上计算整个物理模仿。粒子受重力影响,会与场景几何体产生反弹。这个 WebGL 演示应用了着色器性能,并不需要严格依照 OpenGL ES 2.0 标准要求,因而它可能在某些平台上无奈工作,尤其是在挪动设施上。这将在本文前面探讨。
- https://skeeto.github.io/webg… (source)
它是可交互的。鼠标光标是一个让粒子反弹的圆形障碍物,单击将在模仿中搁置永久性障碍物。你能够绘制粒子能够流过的构造。
这是示例的 HTML5 视频展现,出于必要,它以每秒 60 帧的高比特率录制,所以它相当大。视频编解码器不能很好地解决全屏所有粒子,较低的帧率也不能很好地捕获成果。我还增加了一些在理论演示中听不到的声音。
- 视频播放地址:https://nullprogram.s3.amazon…
在古代 GPU 上,它能够以每秒 60 帧的速度模仿 并且 绘制超过 4 百万个粒子。请记住,这是一个 JavaScript 应用程序,我没有真正花工夫优化着色器,它受 WebGL 的束缚,而不是像 OpenCL 或至多桌面 OpenGL 这样更适宜个别计算的货色。
粒子状态编码为色彩
就像 Game of Life 和 path finding 我的项目一样,模仿状态存储在成对的纹理中,大部分工作是在片元着色器中通过它们之间逐像素映射实现。我不会反复这个设置细节,所以如果你须要理解它是如何工作的,请参考 Game of Life 一文。
对于这个模仿,这些纹理中有四个而不是两个:一对地位纹理和一对速度纹理。为什么是成对的纹理?有 4 个通道,因而其中的每一个局部(x、y、dx、dy)都能够打包到本人的色彩通道中。这仿佛是最简略的解决方案。
这个计划的问题是不足精确性。对于 R8G8B8A8 外部纹理格局,每个通道为一个字节。总共有 256 个可能的值。显示区域为 800×600 像素,因而显示区域上的每个地位不能都显示。侥幸的是,两个字节(总计 65536 个值)对于咱们来说曾经足够了。
下一个问题是如何跨这两个通道编码值。它须要笼罩负值(负速度),并应尽量充分利用动静范畴,比方尝试应用所有 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,因而纹理不是以一维纹理排列,而是放弃方形。粒子由二维坐标索引。
看到间接绘制到屏幕上而不是失常显示的地位或速度纹理十分乏味。这是观看模仿的另一个畛域,它甚至帮忙我发现了一些其它方面很难看到的问题。输入是一组闪动的色彩,但有明确的模式,展现了零碎的许多状态(或不在其中的状态)。我想分享一段视频,但编码比一般显示更不切实际。以下是截图:地位,而后是速度。这里没有捕捉到阿尔法重量。
状态放弃
在 GPU 上运行这样的模仿最大的挑战之一是短少随机值。着色器语言中没有 rand()
函数,因而默认状况下整个过程都是确定性的。所有的状态都来自 CPU 填充的初始纹理状态。当粒子汇集并匹配状态时,可能是一起流过一个障碍物,很难将它们从新拆散,因为模仿会以雷同的形式解决它们。
为了缓解这个问题,第一条规定是尽可能地放弃状态。当一个粒子来到显示区域底部时,会将其移回顶部进行“重置”。如果通过将粒子的 Y 值设置为 0 来实现此操作,那么信息将被销毁。这必须防止!只管在雷同的迭代过程中退出,但显示底部边缘以下粒子的 Y 值往往略有不同。代替重置为 0 的做法是增加一个常量:显示区域的高度。Y 值依然不同,因而这些粒子在碰撞障碍物时更有可能遵循不同的路线。
我应用的下一种技术是通过 uniform 为每次迭代提供一个新随机值,该值被增加到重置粒子的地位和速度。雷同的值用于该特定迭代的所有粒子,因而这无助于重叠粒子,但有助于拆散“流”。这些都是清晰可见的粒子线,都沿着雷同的门路。每一个都会在不同的迭代中退出显示的底部,因而随机值会将它们略微离开。最终,这会在每次迭代的模仿中退出一些新的状态。
或者,能够向着色器提供蕴含随机值的纹理。CPU 必须常常填充和上传纹理,另外还有抉择纹理采样地位的问题,纹理自身须要一个随机值。
最初,为了解决齐全重叠的粒子,在重置时缩放粒子的惟一二维索引,并将其增加到地位和速度中,将它们离开。随机值的符号乘以索引,以防止在任何特定方向上呈现偏差。
为了在演示中看到所有这些,制作一个大圆形来捕捉所有粒子,让它们流入一个点。这将去除零碎中的所有状态。当初革除阻碍。它们都会变成一个严密的团。在顶部重置时,它依然会有一些汇集,但你会看到它们略微离开(增加了粒子索引)。它们将在稍有不同的工夫来到底部,因而随机值施展了作用,使它们更加拆散。几轮之后,粒子应该会再次均匀分布。
状态的最初一个起源是你的鼠标。在场景中挪动它时,会烦扰粒子,并向模仿引入一些噪声。
纹理作为顶点属性缓冲区
在浏览 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 粒子系统铺平道路。
障碍物
一个重要的察看后果是粒子之间不相互作用。这不是一个 n 体模仿。然而,它们的确与世界其它中央互动:它们直观地从这些静止的圆圈上反弹。该环境由另一个纹理示意,该纹理在失常迭代期间不会更新。我称之为 障碍物 纹理。
障碍物纹理上的色彩是曲面法线。也就是说,每个像素都有一个指向它的方向,一个将粒子导向某个方向的流。空隙有一个非凡的常值(0,0)。这不是单位向量(长度不为 1),因而它是对粒子没有影响的带外值。
粒子只需对障碍物纹理进行采样即可查看碰撞。如果在其地位找到法线,则应用着色器函数 reflect
更改其速度。该函数通常用于在 3D 场景中反射光线,但对于迟缓挪动的粒子同样实用。其成果是粒子以天然的形式从圆上反弹。
有时粒子以低速或零速度落在障碍物上或落在障碍物中。为了把它们从障碍物上移开,会将它们朝着失常的方向微微推一推。你会在斜坡上看到这一点,在那里,迟缓的粒子像跳跃的豆子一样摇动着向下自在挪动。
为了使障碍物纹理对用户敌对,理论的几何图形在 JavaScript 的 CPU 端进行保护。这些圆会保留在一个列表中,并在更新时,这个列表中从新绘制障碍物纹理。例如,每次在屏幕上挪动鼠标时,都会呈现这种状况,从而产生挪动障碍物。纹理提供了对几何体的着色器友好访问。两种体现有两个目标。
当我开始编写程序的这一部分时,我构想除了圆以外其它能够搁置的形态。例如,实心矩形:法线看起来像这样。
到目前为止,这些尚未失去施行。
将来想法
我还没试过,但我想晓得粒子是否也能够通过将本人绘制到障碍物纹理上来相互作用。左近的两个粒子会互相反弹。兴许整个 liquid demo 能够像这样在 GPU 上运行。如果我猜测正确,粒子会增大体积,造成碗状的障碍物会填满,而不是将粒子集中到一个点上。
我认为这个我的项目还有一些须要摸索的中央。
参考资料
- A GPU Approach to Particle Physics