当前位置: 首页 > Web前端 > JavaScript

【译文】AGPUApproachtoParticlePhysics

时间:2023-03-27 10:20:27 JavaScript

简介想到了原文中提到的参考教程,于是就去看了一下,发现对理解一些逻辑很有帮助。顺便翻译一下,记录一下。原文:粒子物理的GPU方法起源我的GitHub文本我的GPGPU系列中的下一个项目是一个粒子物理引擎,它在GPU上计算整个物理模拟。粒子受重力影响并从场景几何体反弹。此WebGL演示使用着色器功能,并非OpenGLES2.0规范的严格要求,因此它可能无法在某些平台上运行,尤其是移动设备。这将在本文后面讨论。https://skeeto.github.io/webg...(来源)它是交互式的。鼠标光标是一个圆形障碍物,可以弹开粒子,单击将在模拟中放置一个永久性障碍物。您可以绘制粒子可以流过的结构。这是该示例的HTML5视频表示,出于必要以每秒60帧的高比特率录制,因此它相当大。视频编解码器不能很好地处理全屏中的所有粒子,较低的帧速率也不能很好地捕捉效果。我还添加了一些在实际演示中听不到的声音。视频播放地址:https://nullprogram.s3.amazon...在现代GPU上,它可以以每秒60帧的速度模拟绘制超过400万个粒子。请记住这是一个javascript应用程序,我并没有真正花时间优化着色器,它受WebGL的约束,而不是更适合像OpenCL或至少是桌面OpenGL这样的通用计算的东西。粒子状态被编码为颜色就像生命游戏和寻路项目一样,模拟状态存储在成对的纹理中,大部分工作在片段着色器中通过逐像素映射在它们之间完成。我不会重复此设置的详细信息,因此如果您需要了解其工作原理,请参阅生命游戏一文。对于这个模拟,有四个纹理而不是两个:一对位置纹理和一对速度纹理。为什么要配对纹理?有4个通道,所以它的每个部分(x,y,dx,dy)都可以打包到它自己的颜色通道中。这似乎是最简单的解决方案。这个方案的问题是它缺乏精确性。对于R8G8B8A8内部纹理格式,每个通道是一个字节。总共有256个可能的值。显示区域为800×600像素,所以并不是显示区域的每个位置都能显示出来。幸运的是,两个字节(总共65536个值)对我们来说绰绰有余。下一个问题是如何跨这两个通道对值进行编码。它需要覆盖负值(negativevelocities)并且应该尽量利用动态范围,比如尝试使用范围内的所有65536个值。要对值进行编码,请将该值乘以标量,将其扩展到编码的动态范围内。选择标量时,最高期望值(显示的维度)是编码的最高值。接下来,将动态范围的一半添加到缩放值。这会将所有负值转换为正值,0为最小值。这种表示法称为Excess-K。它的缺点是用透明黑色(glClearColor)清除纹理不能将解码值设置为0。最后,将每个通道视为一个256基数。OpenGLES2.0着色器语言没有按位运算符,因此使用普通的除法和模运算即可完成。我用JavaScript和GLSL做了一个编码器和解码器。JavaScript需要它来写入初始值,并且出于调试目的,它可以读回粒子位置。vec2encode(floatvalue){value=value*scale+OFFSET;floatx=mod(value,BASE);floaty=floor(value/BASE);返回vec2(x,y)/BASE;}floatdecode(vec2channels){return(dot(channels,vec2(BASE,BASE*BASE))-OFFSET)/scale;}JavaScript不同于(0.0-1.0)以上的归一化GLSL值,这会产生一个one-字节整数(0-255),用于打包到类型化数组中。functionencode(value,scale){varb=Particles.BASE;值=值*比例+b*b/2;var对=[数学。地板((值%b)/b*255),数学。floor(Math.floor(value/b)/b*255)];returnpair;}functiondecode(pair,scale){varb=Particles.BASE;返回(((对[0]/255)*b+(对[1]/255)*b*b)-b*b/2)/比例;}更新每个粒子的片段着色器以在该粒子的“索引”处采样位置和速度纹理,解码它们的值,操纵它们并将它们编码回一种颜色以写入输出纹理。因为我使用的是缺少多个渲染目标的WebGL(虽然支持gl_FragData),所以片段着色器只能输出一种颜色。位置在一次通过中更新,速度在另一次通过中更新为两个单独的图。在两次传递完成之前不会交换缓冲区,因此速度着色器(有意)不使用更新后的位置值。最大纹理大小有限制,通常为8192或4096,因此纹理不会排列在一维纹理中,而是保持正方形。粒子由2D坐标索引。看到直接绘制到屏幕上而不是正常显示的位置或速度纹理非常有趣。这是查看模拟的另一个方面,它甚至帮助我发现了一些原本难以发现的问题。输出是一组闪烁的颜色,但具有确定的模式,显示系统的许多状态(或不在其中的状态)。我想分享一个视频,但编码比正常显示更不切实际。这是一个屏幕截图:位置,然后是速度。此处未捕获alpha分量。在GPU上运行此类模拟的最大挑战之一是缺少随机值。着色器语言中没有rand()函数,所以整个过程默认是确定性的。所有状态都来自CPU填充的初始纹理状态。当粒子聚集在一起并匹配状态时,可能会一起流过障碍物,很难让它们分开,因为模拟以相同的方式对待它们。为了缓解这个问题,第一个规则是尽可能多地保存状态。当粒子离开显示区域的底部时,通过将其移回顶部来“重置”。如果这是通过将粒子的Y值设置为0来完成的,则信息将被破坏。必须避免这种情况!显示底部边缘以下的粒子往往具有略微不同的Y值,尽管在同一迭代期间退出。不是重置为0,而是添加一个常量:显示区域的高度。Y值还是不一样的,所以更有可能的是这些粒子在碰撞障碍物的时候会走不同的路径。我使用的下一个技术是通过均匀度为每次迭代提供一个新的随机值,该均匀度被添加以重置粒子的位置和速度。该特定迭代的所有粒子都使用相同的值,因此这对重叠粒子没有帮助,但有助于分离“流”。这些是清晰可见的粒子线,都遵循相同的路径。每一个都在不同的迭代中退出显示底部,因此随机值将它们稍微分开。最终,这会在模拟的每次迭代中引入一些新状态。或者,可以向着色器提供包含随机值的纹理。CPU必须经常填充和上传纹理,还有选择纹理采样位置的问题,这本身就需要一个随机值。最后,为了处理完全重叠的粒子,粒子的唯一2D索引在重置时被缩放并添加到位置和速度,将它们分开。随机值的符号乘以索引以避免在任何特定方向上的偏差。要在演示中看到所有这些,请画一个大圆圈以捕获所有粒子并让它们流入一个点。这将从系统中删除所有状态。现在移除障碍物。它们都会形成一个紧密的团块。在顶部重置时,它仍然会有一些结块,但您会看到它们稍微分开(添加了粒子索引)。它们离开底部的时间略有不同,因此随机值开始发挥作用,使它们更加分离。几轮之后,颗粒应该再次均匀分布。状态的最终来源是您的鼠标。当您在场景中移动它时,您会扰乱粒子并在模拟中引入一些噪声。作为顶点属性缓冲区的纹理在阅读OpenGLES着色器语言规范(PDF)时,我想到了这个项目的想法。我一直想做一个粒子系统,但我一直卡在如何绘制粒子上。表示位置的纹理数据需要以某种方式作为顶点反馈到管道中。通常,缓冲区纹理——由数组缓冲区支持的纹理——或像素缓冲区对象——异步纹理数据副本——可用于此,但WebGL没有这些功能。不可能从GPU获取纹理数据并将其重新加载为每一帧的数组缓冲区。然而,我想出了一个胜过这两个的很酷的技巧。着色器函数texture2D用于对纹理中的像素进行采样。通常,片段着色器将其用作计算像素颜色过程的一部分。但是着色器语言规范提到texture2D也可以用于顶点着色器。就在那时,一个想法打动了我。顶点着色器本身可以执行从纹理到顶点的转换。它的工作原理是将上述2D粒子索引作为顶点属性传递,并使用它们从顶点着色器中查找粒子位置。着色器将以GL_POINTS模式运行,发射点粒子。这是缩写版本:attributevec2index;uniformsampler2Dpositions;uniformvec2statesize;uniformvec2worldsize;uniformfloatsize;//floatdecode(vec2){...voidmain(){vec4psample=texture2D(positions,index/状态);vec2p=vec2(解码(psample.rg),解码(psample.ba));gl_Position=vec4(p/worldsize*2.0-1.0,0,1);gl_PointSize=大小;}真实版本也会在调制颜色时对速度进行采样(慢速移动的粒子比快速移动的粒子更亮)。然而,存在一个潜在的问题:允许实现将顶点着色器纹理绑定的数量限制为0(GL_MAX_vertex_texture_IMAGE_UNITS)。所以技术上顶点着色器必须始终支持texture2D,但它们不需要支持实际纹理。这有点像在没有乘客的飞机上提供餐饮服务。某些平台不支持此技术。到目前为止,我只在某些移动设备上遇到过这个问题。除了在某些平台上缺乏支持外,这还允许模拟的每个部分都保留在GPU上,并为纯GPU粒子系统铺平了道路。对障碍物的一个重要观察是粒子之间不相互作用。这不是n体模拟。然而,他们确实与世界其他地方互动:他们凭直觉从这些静止的圆圈中反弹。环境由另一个纹理表示,在正常迭代期间不会更新。我称之为障碍纹理。障碍物纹理上的颜色是表面法线。也就是说,每个像素都有一个指向它的方向,一个将粒子引导到某个方向的流。Gap有一个特殊的常量值(0,0)。这不是一个单位向量(长度不是1),因此它是一个对粒子没有影响的带外值。粒子只是对障碍物纹理进行采样以检查碰撞。如果在其位置找到法线,则使用着色器函数reflect更改其速度。此功能通常用于反射3D场景中的光线,但同样适用于缓慢移动的粒子。效果是粒子以自然的方式从圆圈反弹。有时粒子会以低速或零速度落在或落入障碍物。要将它们从障碍物中移开,请朝正常方向轻轻推一下。您会在斜坡上看到这种情况,缓慢的粒子像跳豆一样摇晃并自由向下移动。为了使障碍物纹理对用户友好,实际的几何图形在CPU端用JavaScript维护。圆圈保存在一个列表中,更新时,障碍物纹理将从该列表中重新绘制。例如,每次鼠标在屏幕上移动时都会发生这种情况,从而产生移动障碍物。纹理提供对几何体的着色器友好访问。两种表示都有两个目的。当我开始编写这部分程序时,我设想了除了圆形之外还可以放置的其他形状。例如,一个实心矩形:正常看起来像这样。到目前为止,这些还没有实施。未来的想法我还没有尝试过,但我想知道粒子是否也可以通过将自己绘制到障碍物纹理上来进行交互。附近的两个粒子相互反弹。也许整个液体演示可以像这样在GPU上运行。如果我猜对了,颗粒会变大,形成碗状物的障碍物会被填满,而不是将颗粒集中到一个点上。我认为这个项目还有一些值得探索的地方。参考粒子物理学的GPU方法