体验高效的Flash位图开发技巧 —— 雪球粒子

阅读本文需要掌握的基础:
* ActionScript 3.0编程基础
* 空间立体几何与三角函数基础
* 了解物体的运动规律

使用的开发工具:
* Flash Professional

如果你是一个设计师,而恰巧对ActionScript编程感兴趣,你可能会尝试去开发一些比较酷的Flash特效。在一些对视觉效果要求比较高的行业比如广告,如何高效的实现一个效果往往比如何准确无误地实现一个页面逻辑要更加重要。但是一直以来,运行效率一直是Flash的核心问题与技术瓶颈。这篇文章会通过一个雪球粒子的例子来给大家介绍一个开发思路,一个如何使用位图来高效实现运算的思路。

需求
我们现在要做一个比较好玩儿的雪球效果,在舞台上有一个可以通过鼠标来控制旋转的球体,球是由若干个独立雪花粒子组成,并且近处的雪花清晰可见,远处的雪花模糊。雪球不断膨胀,膨胀到边缘的雪花粒子消失并且重置回到球的中心。要求雪花粒子数量要足够多,并且一定要流畅。

分析
这个例子需要解决如下几个问题:
1,3D空间的旋转控制,需要对每个粒子的空间属性进行操作。
2,绘图的方案,需要用一个高效的方法来处理上千个粒子的运算。
3,利用第一点的属性,在第二点的渲染能力之内尽可能地修饰粒子的显示效果

方案
3D空间的旋转控制

对于大量粒子的运算,一个节省内存的技巧,是给每个粒子建立尽可能小的对象实例。接下来要考虑的是,空间内的任何一个粒子,它应该具备的属性有哪些。在这个例子中,由于是围绕定点的三维旋转,所以除了直角坐标系x,y,z之外,还可能会需要极坐标属性,模与方向角。

如图1所示,设M为空间内任意一点(x,z),分别做M到平面XOZ的垂线MM’,以及M到X轴的垂线MN,可以看到线段OM的长度是M点在极坐标系中的模。这里为了方便运算,采用OM在XOZ的投影OM’与X轴的夹角A来做M点的水平方位角,同理,B为垂直方位角。通过分析发现,如果M围绕Y轴旋转,实际上是水平方位角A发生了变化,如果M围绕X轴旋转,实际上是垂直方位角B发生了变化。所以我们就可以得到一个结论:点M在空间中的旋转可以通过改变M的水平方位角与垂直方位角来控制。


图1,空间内任意点M的方位角

方位角angleX与angleY的值可以通过反正切函数来很容易的得出:
angleX = Math.atan2(z,x);
angleY = Math.atan2(z,y);

现实中方位角的改变是同时进行的,可是我们在运算中可以将它分解为两个部分,因为无论是先改变水平方位角或者先改变垂直方位角,另一个方向的方位角都会随之变化,所以无论按什么顺序分解都可以。
以改变水平方位角A为例,由于Y值不变,模不变,所以OM’旋转后的值也不变,这样新的X和Z值可以通过新的水平方位角得出:
var dxz:Number = Math.sqrt(x*x + z*z) ;
x = dxz * Math.cos(angleX);
z = dxz * Math.sin(angleX);

由于M点的坐标发生了变化,所以垂直方位角B的值也变为:
angleY = Math.atan2(z,y);

接下来是垂直方位角的改变以及新的坐标值:
angleY += valueY;
var dyz:Number = Math.sqrt(y*y + z*z);
y = dyz * Math.cos(angleY);
z = dyz * Math.sin(angleZ);

这样就完成了一个赋值循环,这个算法得出的运动效果是最真实的三维旋转

但是,设计的过程往往是计划与灵感并存的过程,有的时候最终的结果会来源于意想不到的发现。这个例子里的解决方案并非是上面的改变方位角的方法,而是采用运动分解来直接改变物体坐标,然后通过更新方位角来形成赋值循环。由于运动分解本身存在曲线微分的误差,所以这种运动与真实的三维旋转相比也有很大的误差。但是这个误差意外地带来了一种膨胀和流体的效果,使得雪球的运动模式更加诡异多变,所以我最终采用了这种方案。


图2,水平方向的运动分解

下面来看M的运动规律,虽然M在三维空间的自由度是3,但是因为我们只控制绕X轴与绕Y轴两个方向的旋转,所以M在我们的例子里的运动方向实际上可以分解为两个切线方向。如图2所示的绕Y轴旋转方向,可以看作是OM’切线方向OM”,然后再对OM”进行XZ轴的分解,得出沿Z轴的分量M’Q和沿X轴的分量M’P。由此可见,每一个绕Y轴方向的旋转,也就是每一次水平旋转,都会改变粒子在X轴与Z轴的值;同理,每一次垂直旋转,都会改变粒子在Y轴和Z轴的值。假设每一次水平旋转速度为veLocityX,垂直旋转速度为veLocityY,那么用ActionScript来表示的坐标改变公式就是:
x += veLocityX * Math.sin(angleX);
y += veLocityY * Math.sin(angleY);
z -= veLocityX * Math.cos(angleX) + veLocityY * Math.cos(angleY);

当点M的坐标发生变化之后,重新用三角函数计算方位角的值:
angleX = Math.atan2(z,y);

这样,一个赋值循环就形成了:当外界控制改变了运动速度veLocityX和veLocityY,坐标X,Y,Z发生变化,方位角angleX和angleY发生变化,以备下次赋值时用来计算坐标。粒子系统中所有的点都通过这样的方式完成了一次赋值,一个时间帧的运算也就完成了。其他的一些计算比如边界控制,加速度控制等都不是本文的重点,大家可以在代码中阅读并掌握。

高效的位图绘制API —— copyPixels
如果你还是在用MovieClip或者Sprite来绘制粒子,那么你一定达不到满意的效果,因为在粒子的世界里,displayObject已经完全Out了,代替它的是位图数据BitmapData。在这个例子里我写了一个方法updateBySprite,朋友们可以打开那个方法来看看利用Sprite的绘制效率有多么的糟糕,100-200个粒子还算流畅,一旦超过200就已经勉强,如果再提升到500,立刻卡的不行,帧率下降到10以下,这还是在没有加透明度以及模糊滤镜的前提下。所以要高效,必须用位图绘制。

BitmapData.draw是比较常用的绘图API,它接受的参数是接口IBitmapDrawable,包括了一切可以被绘制的显示对象包括影片,Sprite甚至视频。但是BitmapData.draw的效率并非很高,可以用它来一次性采集位图源,如果要频繁处理大量粒子的绘制,draw绝非最佳选择。BitmapData.copyPixels是用来绘制大量粒子的高效API,它的运行效率是BitmapData.draw的数倍,使用这个API,可以处理上千个粒子的绘制,并且帧率保持在24之上。下面我来介绍如何使用copyPixels来完成雪花粒子的绘制。

BitmapData.copyPixels(sourceBitmapData:BitmapData,sourceRect:Rectangle,destPoint:Point,alphaBitmapData:BitmapData=null,alphaPoint:Point=null,mergeAlpha:Boolean=false);

参数sourceBitmapData接收的是一个BitmapData对象,如果雪花粒子单元为一个MovieClip,那么必须先经过BitmapData.draw采集位图源。
alphaBitmapData指定了一个位图来定义绘制的透明通道,这个例子里我们需要用到它来实现对不同距离雪花的清晰度的区分。本例调用copyPixels的代码如下:


//清空位图,重新绘制
bmp.bitmapData = new BitmapData(ball.width,ball.height,true,0x00);
for(var i:int=0;i< flowers.length;i++){
//SNowFlower为一个粒子的运算替身,用来计算粒子的坐标
var sf:SNowFlower = flowers[i] as SNowFlower;
sf.updateAccel(-ball.mouseX/10,-ball.mouseY/10);
//获取粒子的纵深比例,用来选择不同的位图源显示粒子,位图源之间有透明度以及模糊度的区别
var index:Number = Math.floor((ball.width/2 + sf.z)*stepCount/Math.abs(ball.width));
var tempBmd:BitmapData = bmds[index] as BitmapData;
//绘制粒子,alphaBitmapData为位图源本身
var pt:Point = new Point(ball.width/2 + sf.x-tempBmd.width/2,
ball.height/2 + sf.y-tempBmd.height/2);
bmp.bitmapData.copyPixels(tempBmd,tempBmd.rect,pt,tempBmd,new Point(),true);
}

修饰显示效果
在上边的代码里,有一个技巧我需要跟大家介绍一下。本例中调用copyPixels的函数一个叫enterFrameHandler侦听器,它侦听的是Event.ENTER_FRAME事件,每一帧调用一次。所以在这个侦听器中的计算量大小直接导致了整个应用程序运行效率的高低。我们的原则是尽量减小一些不必要的运算,比如透明度和模糊,但是这些效果又是必要的修饰,所以我的处理方法是在程序开始一次性复制20个位图源,然后按照从0到1给它们进行不同的透明度以及模糊滤镜的处理。这样,我只要在enterFrameHandler里算出当前的粒子处于什么纵深位置,就可以轻松地将已经准备好的位图源调出来使用,省掉了大量的计算,也提高了运行效率。这个处理方法代码如下:


//这个数组用来存放位图源
bmds = new Array();
//采集雪花
var flr:MovieClip = new flower();
var matrix:Matrix = new Matrix();
var cf:ColorTransform = new ColorTransform();
matrix.tx = flr.width/2;
matrix.ty = flr.height/2;

for(var i:int=0;i< stepCount;i++){
var bmd:BitmapData = new BitmapData(flr.width,flr.height,0x00);
//调整每个位图源的透明度
cf.alphaMultiplier = .2+ .8 * i/20;
bmd.draw(flr,matrix,cf);
//调整每个位图源的模糊效果
bmd.applyFilter(bmd,bmd.rect,new BlurFilter(.3*(stepCount-i),.3*(stepCount-i),3));
bmds.push(bmd);
}

这样,我可以轻松地渲染4000个雪花粒子,并且流畅地通过鼠标来控制它的旋转:

 

源码可以到我的资源里下载! 

http://download.csdn.net/detail/meetlunay/4547205

 

 

 

 

 

 

 

 

 

 

文章来自http://jamesli.cn/blog/?p=561

相关文章

  译序:JWMediaPlayer是开源的网页使用的Flash播放器。本...
    Flash编程原理都是只能将1写为0,而不能将0写成1.所...
 上传setenvgatewayip192.168.1.1;setenvserverip192.168.1...
Error:FlashDownloadFailed-"Cortex-M3"出现一般有...
jPlayer是一个用于控制和播放mp3文件的jQuery插件。它在后台...
#ifndef__FONTUPD_H__#define__FONTUPD_H__#include"sy...