[Stage3D]GPU渲染的喷泉粒子

您需要把Flash Player升级到11版本才能正常播放示例程序。

如果播放动画的帧率较低,那么有可能是您的显卡不被Flash Player所支持,Flash Player会采用CPU来对画面进行渲染。

源码:https://files.cnblogs.com/flash3d/partic.rar

在详细介绍该粒子效果编写方法之前,先让我们简单了解下Stage3D开发基本流程与注意事项。

1. 获取Stage3D对象

Stage3D对象用于显示通过Stage3D技术渲染出来的图像,我们可以通过类似

stage.stage3Ds[0]

的方式获取Stage3D对象,Flash总共提供四个独立的Stage3D对象。

2. 获取Context3D对象

Context3D对象是我们使用Stage3D技术主要操作对象。该对象类似一个大工厂,我们可以向他申请空的顶点缓冲(VertexBuffer3D),顶点索引(IndexBuffer3D),着色器程序(Program3D)等,而后向全新的对象内装入数据再通过Context3D上传至GPU。

Context3D对象并不能通过new来创建,也不能立即申请到。要想获取Context3D对象则需通过

stage3d.requestContext3D()

的方式发起申请请求,而后监听Event.CONTEXT3D_CREATE事件发生才算申请成功。此时则可通过

stage3d.context3D

获取Context3D对象了。这里需要注意Event.CONTEXT3D_CREATE事件可能多次发生,假如其他程序中断了Flash Player的GPU使用权后Flash Player重新获得其使用权,则会再次发生Event.CONTEXT3D_CREATE事件。假如你在Event.CONTEXT3D_CREATE事件的回调中做了一些初始化工作,那么就要小心了,如果你不及时移除监听,就有重复初始化的隐患。

3. 创建Program3D对象并写入程序

通过

context.createProgram()

的方式,可以获得一个空的Program3D对象。Program3D对象负责储存着色器程序。有些朋友可能对着色器程序的概念和工作原理还不是很清楚,这里我简单介绍下。

着色器程序(AGAL写的程序)分为两部分:顶点着色器程序(Vertex Shader),段着色器程序(Fragment Shader)。他们均被上传至GPU运行。顶点着色器程序负责处理顶点缓冲数据并将处理好的数据写入到位置输出寄存器(op)并且将部分必须的中间数据储存到变量寄存器留给段着色器使用,段着色器则是根据从顶点着色器传来的变量,结合纹理数据,计算合成某个像素需要输出的值。

关于用AGAL着色器的语法,请参考http://bbs.9ria.com/viewthread.php?tid=79747

关于Stage3D的最简单“Hello World”程序:http://www.pixelbender.cn/?p=381

所以,从程序的角度看,貌似顶点着色器先被执行,而后再执行段着色器。但是请注意,实际上在GPU中,顶点着色器与段着色器的运行次数是不一样的,这也是为什么在Stage3D的“Hello World”程序中所绘制的是一个渐变图像而不是四个像素点了。

实际上GPU是首先计算完全部的顶点缓冲(可能有并行运算),而后对计算出来的顶点数据投影到2D屏幕上(计算出来的顶点原本是三维点,期间可能还进行了一些消隐等操作),接下来对这些顶点围成的三角形进行栅格化(变成位置确定的像素点,但尚未有颜色),最后才是对栅格化后的像素执行段着色器程序,而其所需的变量寄存器值则通过像素位置关系内插计算得到的渐变值。

如图展示了渲染管线的处理过程

 如下示例展示了一个三角形的绘制过程

我们可以通过Program3D对象的upload方法上传程序,该方法接收两个参数,第一个是顶点着色器程序(Vertex Shader),另外一个是段着色器程序(Fragment Shader)。但是请注意,upload的两个参数类型是字节数组(ByteArray)而非字符串(String),所以我们需要AGALMiniAssembler对象来帮助我们把字符串的着色器程序变成字节数组。通过AGALMiniAssembler对象的assemble方法可以达到上述功能。该方法第一个参数是程序类型,可能值是两个,一个是"vertex",表示第二个参数是顶点着色器程序,另外一个是"fragment",代表第二个参数是段着色器程序。

4. 创建顶点缓冲数据

顶点缓冲数据可以看成n行m列的二维数组。其中每行数据可以产生一个三维模型中某个三角形的顶点,该行的数据值决定着该顶点输出的坐标(具体如何决定则看顶点着色器程序,也有可能部分数据不起作用,而是间接对段着色器起作用)。而顶点缓冲数据的行数则决定顶点的个数。

我们可以通过

context.createVertexBuffer(行数, 列数)

来申请一个空的顶点缓冲包装对象。之后我们需要一个“行数 * 列数”长度的Vector.<Number>类型数组来写入我们的顶点数据。

可以通过

vertex.uploadFromVector(数组, 行偏移量, 行数)

上传数据。通过

context.setVertexBufferAt(寄存器序号, vertex, 列偏移量, 数据长度)

设置一行中的顶点数据如何分配给寄存器。其中寄存器序号就是在agal中va后所带的数字,如寄存器序号是1,则可通过va1访问设置进去的数据。列偏移量表示要设置到寄存器中的数据区间其第一个浮点数是处在该行的第几列(0开始),数据长度则表示要设置几个浮点数到寄存器中,其值是个字符串,类似"float2","float3"等,最大是"float4",因为一个寄存器最多只能储存四个浮点数。
以下例子演示如何将顶点数据设置到寄存器中并在AGAL中访问。

这里我们建立一个最简单的三角形作为模型,一个三角形拥有三个顶点,故顶点缓冲有三行数据,每行数据中,我们需要三角形的坐标,以及顶点处的颜色(数据的数量,意义,作用看具体程序而定),于是我们决定用两个浮点数表示坐标位置(X,Y),用三个浮点数表示颜色值(R,G,B),所以顶点缓冲数据应该是3行5列的,其数据长度是15。通过如下代码

vertex.uploadFromVector(new<Number>[0, 1, 0, 0.5, 1, -1, -1, 0.5, 1, 0, 1, -1, 1, 0, 0.5], 0, 3)

可以将如下数据设置到顶点缓冲中去了。

0 1 0 0.5 1
-1 -1 0.5 1 0
1 -1 1 0 0.5

通过如下两行代码

context.setVertexBufferAt(0, vertex, 0, "float2");
context.setVertexBufferAt(1, vertex, 2, "float3");

可以将蓝色部分的数据设置到va0中,把红色部分数据设置到va1中。

假设顶点着色器程序正在处理第三行,此时访问va0.x为1,va0.y为-1,va1.x为1,va1.y为0,va1.z为0.5。

着色器寄存器可容纳四个浮点数,当设置进去的数据长度不够时(比如设置的是"float2"),则系统默认帮你填上默认值,寄存器前三个浮点数的默认值均为0,第四个浮点数默认值为1。所以此时va0.z为0,va0.w为1。请注意,这个默认值是执行setVertexBufferAt的时候就设置进去的,而非到达GPU后才产生。GPU中的临时寄存器在未被赋值之前是访问不到其值的(直接挂了)。所以对于未被设置过的vt0

mov vt0.xy, va0.xy
mov vt1.x, vt0.z //挂了
mov vt0, va0
mov vt1.x, vt0.z //没挂

以上程序产生不同的结果。

顶点着色器程序从头到尾运行一次只处理一行数据,而且只能访问本行数据,各行数据无法相互影响。

5. 创建纹理数据

纹理数据在段着色器运行时被采样,比较常见用处是UV映射,将纹理作为模型贴图,更加高级的用法则比如通过在同个或者多个纹理的多个点上采集像素并通过某种算法合成而产生特殊效果。

可以通过

context.createTexture(宽, 高, "rgba", false);

来获得空的纹理包装对象。

之后向纹理包装对象上传位图

texture.uploadFromBitmapData(位图)

最后通过

context.setTextureAt(寄存器序号, texture)

来设置到指定的纹理寄存器上。

在AGAL中,可以通过类似

tex ft1,v0,fs0<2d,linear,nomip>

的方法把像素采集到寄存器中,fs0是序号为0的纹理寄存器,可以访问到通过setTextureAt设置到0的纹理。<2d,linear,nomip>是采样属性,其依次可能值为

尺寸:2d,3d,cube
投影:nomip,mipnone,mipenearest
滤镜:nearest,linear
拷贝:repeat,wrap,clamp
v0代表要采样的坐标,ft1则是储存采样结果。

6. 设置着色器程序常数

着色器程序的特性与数学函数非常类似,具有纯数据计算的特性。故常数对于着色器的意义相当于常数对于数学函数的意义一样。

比如函数“y = ax”展现出的是y随x线性变化的特征,但是常数a却控制着变化速度的快慢。

通过类似

context.setProgramConstantsFromVector("vertex", 0, new<Number>[0, -0.5, 0.2, 0])

的代码可将常数设置到常数寄存器中。函数第一个参数代表设置的是顶点着色器的常数还是段着色器的常数,"fragment"代表段着色器,设置进入的常量可通过类似fc0的形式访问,"vertex"代表顶点着色器,设置进入的常量可通过类似vc0的形式访问,vc后的数字则是由这个函数的第二个参数确定,表示要设置到哪个标号的寄存器内。第三个参数是常量数据,他是一个不超过4个长度的Vector.<Number>类型数组。其四个值分别对应着vt0.x,vt0.y,vt0.z,vt0.w。

对于as来说,改变常量是非常省力的(一行代码而已),而改变顶点缓冲数据则非常费劲(需要遍历整个数组)。所以如果希望通过Stage3D技术产生流畅的动画,则必须通过不断改变常量来完成,而不能改变顶点缓冲数据的值,否则GPU加速也就没有意义了,这也是最考验着色器设计能力的地方。

7. 创建顶点索引

顶点索引数据是一个n行3列的数据。其一行可绘制一个三角形,一行内的三个数据分别是三角形三个顶点的索引,这里索引所引用的就是顶点缓冲数据里面的某一行(代表某个顶点),索引值便是顶点缓冲数据的行号。

可通过

context.createIndexBuffer(索引数)

来创建空的顶点索引包装对象。这里注意其参数是索引数(即n*3),而非行数。

通过

index.uploadFromVector(Vector.<uint>类型数组, 起始索引偏移量, 索引数)

上传索引数据。在绘制3D画面时,可以将index对象传入context.drawTriangles绘制出所有指定三角形。

 

以上简单介绍了Stage3D技术开发的基本流程,当然Stage3D还有其他功能需要大家深入去研究。

有了上面的基础,下面讲一下Stage3D喷泉粒子效果的制作要点,可能讲的不对或者有更好的方案,还请各路大大多多指教。

1. 粒子模型

Stage3D并非BitmapData,无法直接将粒子以像素的形式设置到画面上,故考虑以一个小三角形作为粒子模型。所以总共的顶点数应该是粒子数的三倍。

2. 动画的实现

考虑到要充分发挥Stage3D的优势,所以动画的实现,也就是粒子位置随时间的连续性改变也不应该让as去实现。否则粒子的位置坐标就必须由as来计算并设置,那么就必须遍历粒子数组,开销大大滴。

所以我的方案是给着色器传一个时间常量,粒子的坐标位置随时间沿着某个曲线运动。

由于是喷泉效果,所以采用的是抛物线轨迹(捂脸~~太没科技含量了)

抛物线原型:y = - x^2

引入一个常量使其变形经过原点的上抛曲线:y - c^2 = -(x - c)^2

由于还考虑到喷泉中每个粒子的喷发角度不一样故还引入常量d:y = c^2 - (d * x - c)^2

d表示x轴缩放比例的倒数,若d为负则抛物线方向向左。

有了曲线方程,则下面需要x关于时间的方程了,根据物理规律,由于喷洒的高度相同,故粒子从升起到落地的时间相同,那么对于x来说就是从0位置移动到2c/d的时间相同。那么当给定0到1范围的时间变量,无论2c/d是多少,经过相同的时间完成x与2c/d的比例也是相同的,于是有了这个方程:x = 2 * c / d * t

由于我们需要不同的粒子处在抛物线运动的不同时刻,这样才会看起来有前后次序的感觉,于是我们还需要给这个t再加上一个时间常量得到x = 2 * c / d * (t + o)

再者需考虑t + o需要在0至1范围内,并且超过1后回归为0,这样才会有粒子的生命结束与重生。所以这里动用到agal里frc这个运算符,其功能是对源操作数取小数,最后得到的公式为:

x = 2 * c / d * frc(t + o)

将该时间方程代入上述曲线方程得到y与时间的关系方程:y = c^2 - (2 * c * frc(t + o) - c)^2

此方程中常量c确定抛物线的高度,由于全部粒子最大高度一致,所以c可设置为全局常量;常量d确定粒子的运动方向与跨度,由于各粒子不相同,所以作为曲线描述信息保存到顶点缓冲中;o为初始时间偏移量,故各粒子也不同,均保存到顶点缓冲中。

3. 粒子模型的形状保持

粒子的运动概念上是以三角形为单位,所以三角形的三个顶点相对位置应该保持不变才能防止粒子形状发生改变,也就是说,每次运动粒子的三个顶点必须加上相同的该变量。

所以我设定的顶点缓冲数据结构是这样的

va0 va1
p1.x p1.y o d
p2.x p2.y
p3.x p3.y

p1 p2 p3分别是三角形的三个顶点坐标,接下来的每三行,其值都与该三行的va0值对应相同,这三个点的数据确定了粒子的形状。但是va1作为描述运动规律的数据,则是三行都相同,但是与其他的三行不相同。这体现了这三行的数据是共同描述一个粒子,而粒子的每个顶点其运动规律都相同,才不至于形状改变。

结合上文推导的运动方程,我们得到如下顶点着色器代码

mov vt2, vc1
mov vt0, va0
add vt1.x, vc0.x, va1.x //t + o
frc vt1.x, vt1.x //frc(t + o)
mov v0, vt1.x //将运动百分比传到段着色器
mul vt1.x, vt1.x, vt2.y //c * frc(t + o)
mul vt1.x, vt1.x, vt2.w //2 * c * frc(t + o)
div vt0.x, vt1.x, va1.y //x = 2 * c / d * frc(t + o)
sub vt1.x, vt1.x, vt2.y //2 * c * frc(t + o) - c
pow vt1.x, vt1.x, vt2.w //(2 * c * frc(t + o) - c)^2
pow vt1.y, vt2.y, vt2.w //c^2
sub vt0.y, vt1.y, vt1.x //y = c^2 - (2 * c * frc(t + o) - c)^2
add vt0.xy, vt0.xy, va0.xy //粒子坐标累加到顶点坐标
add vt0.xy, vt0.xy, vc0.yz //原点做一定偏移
mov op, vt0 //输出

而段着色器上,只是做了一点颜色渐变。主要是根据运动百分比来线性增加颜色的亮度,符合水喷出去就白了的规律(囧~)

mov ft0, fc //获取设定的颜色常量
//给三原色加上运动百分比
add ft0.x, ft0.x, v0.x
add ft0.y, ft0.y, v0.x
add ft0.z, ft0.z, v0.x
mov oc, ft0 //输出

程序源码在最上面。。。

posted on 2012-02-01 05:01  Clifford  阅读(8983)  评论(3编辑  收藏  举报

导航