【AS3 Coder】任务四:噪音的魅力(上)

使用框架:AS3
任务描述:使用AS3中BitmapData的noise方法以及perlinNoise方法构建自然景观效果以及其他一些比较cool的效果
难度系数:2

本文章源码下载:www.iamsevent.com/upload/AS3Coder4/AS3Coder4_1.rar

音污染是四大污染之一,被人们深恶痛绝啊,我就很讨厌隔壁一大早就开始响起的装修声,打断寡人的春梦,真是的,正要办正事呢……不过,在计算机领域也存在一种噪声,我不明白它为什么要取noise这个名字,但是我们不必纠结于名字这种虚的东东(就像我们不知道为什么要称大便为大便一样),我们只关心它能干吗,还有S兄这次为什么要介绍这玩意儿。
       其实在初涉Flash AS3领域的时候我就对BitmapData中的noise方法以及perlinNoise这两个方法有好奇心,但是当时的参考资料过少,也就没怎么管它了。不过在09年的时候出了一本书叫做《Foundation ActionScript 3.0 Image Effects》(如下图)的书,中文翻译过来应该是《ActionScript3.0基础图像效果教程》,由于其封面是一只山羊,所以叫它山羊书得了。

但是由于一直没有出中文版的,所以就一直没有去看过。然而最近我们的拉登兄在他的博客开始了义务翻译这本书,大家有空可以给他捧捧场,里面还附带了英文原版电子书以及配套源码的下载地址哦(http://blog.sina.com.cn/s/blog_4bfac6ef0100wwyn.html)。在他的宣传下,我去他的博客下载了英文原版看,现在的我英文水平已经提高不少,所以对于英文书籍的阅读是没有问题的。在看过之后发现,这本书上对于噪声(noise)这种图形算法的介绍是如此之详细,让我不由得被吸引住了,在更加深入了解之后我已对AS3中BitmapData类自带的两个噪声生成方法noise, perlinNoise有了初步了解并为它们所能创建出的美丽效果震撼不已(难怪最近老是蛋疼)。那么今天我将带领大家一起来与我分享这种超爽的体验。
(PS:不好意思,本教程未使用视频方式来表达,让一直怂恿我使用视频形式做教程的朋友们失望了,sorry,sorry )
        先来了解一下noise的概念吧。数据噪声(Digital Noise)是一种像素的随机化表现,它们的亮度及颜色属性会有无数种组合可能性,因此,通常无法预知一副由噪声生成的图像的样子是怎么样的。既然结果不可预料,那么为什么我们还需要使用噪声来生成图片呢?在现实生活中,没什么东西是完美的,如果一个东西太四四方方,形状十分规则平整那么我们会觉得其不够真实。那么此时我们就可以为图像增加一些噪声图案部分以使其看起来更加接近现实。更多的时候,噪声可以让我们很容易地在计算机图像中模拟一些自然效果,比如在放映老电影时大屏幕上运动的颗粒,布满星星的夜空等。这里就不去探讨noise算法的历史了,接下来直接看看AS3中的BitmapData提供的noise方法的使用方式。
        在BitmapData中存在一个noise方法能够帮助我们很直接,很快速地创建噪声图像。但是在BitmapData中使用noise方法不同于在PhotoShop中运用噪声,在BitmapData中掉用noise方法后将会打乱整个图像,因此如果你只想对原图像的一小部分运用噪声,那么请新建立一个BitmapData实例并在它生成噪声图像后使用像素拷贝技术(CopyPixels、draw等)来做到。让我们来看看noise方法的签名:

public function noise(randomSeed:int, low:uint = 0, high:uint = 255, channelOptions:uint = 7, grayScale:Boolean = false):void
randomSeed        要使用的随机种子数。如果您保持使所有其他参数不变,可以通过改变随机种子值来生成不同的伪随机结果。杂点函数是一个映射函数,不是真正的随机数生成函数,所以它每次都会根据相同的随机种子创建相同的结果。 
low、high        指定要为每个通道生成的颜色值区间
channelOptions        指定将生成杂点的颜色通道,可用值为BitmapDataChannel中常量
grayScale        如果该值为 true,则会通过将所有颜色通道设置为相同的值来创建一个灰度图像。将此参数设置为 true 不会影响 Alpha 通道的选择。 

由于参数较少,所以就不需要多解释什么,直接来看一个例子,这个例子是山羊书里面的一个例子,为我们展示了如何实现电视机无信号的图像(点击图片在线预览)

package {

        import flash.display.Bitmap;
        import flash.display.BitmapData;
        import flash.display.Sprite;
        import flash.events.Event;
        import flash.utils.getTimer;

        [SWF(width=400, height=300, backgroundColor=0x000000)]

        public class NoiseTest extends Sprite {

                private var _bitmapData:BitmapData;

                public function NoiseTest() {
                        _bitmapData = new BitmapData(stage.stageWidth, stage.stageHeight);
                        makeNoise();
                        addChild(new Bitmap(_bitmapData));
                        addEventListener(Event.ENTER_FRAME, onSpriteEnterFrame);
                }

                /**
                * 为bitmapData生成噪声图像
                */
                private function makeNoise():void {
                         //第一个参数randomSeed每次取到的都不一样,因此每次调用此方法都会生成
                        //一副全新的噪声图像。第二个、三个参数决定了像素颜色的变化范围,以至于
                        //像素颜色不会太暗(越接近0越暗,越黑)。第四个参数保留了默认值,表示
                        //生成的随机像素颜色包含RGB三个通道。第四个参数设置为true,则全部像素都
                        //是经过灰化了的
                        _bitmapData.noise(getTimer(), 100, 255, 7, true);
                }

                /**
                * 每帧都生成新的一副噪声图像
                */
                private function onSpriteEnterFrame(event:Event):void {
                        makeNoise();
                }

        }
}

 上述代码主要部分在于对noise各个参数的设置上,对于各个参数设置的意义我都在注释中写明了,你可以更改一些参数来看看效果怎样,比如把最后一个参数grayScale设置为false,这样将会看见你的“电视屏幕”中的雪花点变成彩色的了……值得注意的是,调用noise方法是会花不少时间来生成噪声杂点图像的,所以你每帧都调用noise去生成新的噪声杂点图像会比较消耗CPU,在这个例子中由于杂点图像生成目标BimapData的尺寸不大,所以CPU使用率还可以,大概在10以下,若是你增大BitmapData的尺寸会发现CPU使用率也会水涨船高。

      对于noise的应用我们就讲这一个例子,因为它的使用范围实在是狭窄,为什么狭窄?因为noise创建的杂点是平均分布的,因此看起来哪里都一样。为了创建出更加真实的自然效果,我们可以使用柏林噪声(perlinNoise)来做到。使用柏林噪声生成算法,我们可以创建出近乎“自然”的杂点。下面是山羊书中对于柏林噪声的描述文字:

 Perlin 杂点生成算法会内插单个随机杂点函数名为 octave 并将它们组合成一个函数,该函数生成多个看起来很自然的随机杂点。就像音乐上的八音度,每个 octave 函数的频率都是其前面一个 octave 函数频率的两倍。Perlin 杂点被描述为“杂点的碎片总和”,因为它将多组杂点数据与不同级别的细节组合在一起。 您可以使用 Perlin 杂点函数来模拟自然现象和风景,例如,木材纹理、云彩或山脉。在大多数情况下,Perlin 杂点函数的输出不会直接显示出来,而是用于增强其他图像并为其他图像提供伪随机变化。 简单的数字随机杂点函数通常生成具有粗糙的对比度点的图像。这种粗糙的对比度在自然界中通常是找不到的。Perlin 杂点算法混合了在不同的详细级别上进行操作的多个杂点函数。此算法在相邻的像素值间产生较小的变化。

当然,没有哪个天才是一看这段抽象的描述文字就能明确地理解perlinNoise的使用方式,我们还是一步步地来看,首先自然是对于其参数的理解:
public function perlinNoise(baseX:Number, baseY:Number, numOctaves:uint, randomSeed:int, stitch:Boolean, fractalNoise:Boolean, channelOptions:uint = 7, grayScale:Boolean = false, offsets:Array = null):void
baseX, baseY        要在 x 、y方向上使用的频率
numOctaves                要组合以创建此杂点的 octave 函数或各个杂点函数的数目。octave 的数目越多,创建的图像越细腻。octave 的数目越多,需要的处理时间也会越长。
randomSeed                要使用的随机种子数。如果您保持使所有其他参数不变,可以通过改变随机种子值来生成不同的伪随机结果。Perlin 杂点函数是一个映射函数,不是真正的随机数生成函数,所以它会每次根据相同的随机种子创建相同的结果
stitch                如果该值为 true,则该方法将尝试平滑图像的转变边缘以创建无缝的纹理,用于作为位图填充进行平铺。
fractalNoise                如果该值为 true,则该方法将生成碎片杂点;否则,它将生成湍流。带有湍流的图像具有可见的不连续性渐变,可以使其具有更接近锐化的视觉效果,例如火焰或海浪。 
channelOptions                杂点所用颜色通道
grayScale                是否创建灰度图像
offsets                与每个 octave 的 x 和 y 偏移量相对应的点数组。通过操作这些偏移量值,您可以平滑滚动 perlinNoise 图像的图层。偏移数组中的每个点将影响一个特定的 octave 杂点函数。 

      仅通过文字的解释那是绝对不能说服我军的,我们只相信我们的狗眼不是吗?来吧,给个perlinNoise方法的参数测试结果在线试验程序(点击图片打开),是老外写的哈:

聪明人一看就知道,左边是参数设置的面板,而右边是bitmapData.perlinNoise方法根据左边这些参数生成的图像,你可以拖拽这个图像进行尺寸缩放,还可以在上面调整背景颜色,很人性化,嗯……
      如果你仔细试验,你会发现以下一些规则:
1.把baseX值调低则图像会变窄,即单位长度中存在的“彩色团”数量少了,反之,则会让图像变宽。而对于baseY也是一样的规则,值小则单位高度中存在的“彩色团”数量少……因此我们可以姑且把baseX和baseY作为拉伸图像的元凶
2.octaves的值越高图像越细腻,反之则粗糙。因为octaves值决定了使用柏林噪声算法生成的图像层数量,层越多看起来自然越细腻,就像你过滤纯净水一样,水的纯度自然是和你过滤次数成正比的,但是次数越多消耗的时间越长
3.randomSeed的值就不必多说了,反之每次改变后都会出现不一样的图像,但是需要注意的是,如果你起始值是1,然后随便改一个值后生成一副新图像后再把randomSeed值改回1会发现生成的还是起始情况下的图像,所以这就叫做“伪随机”,即生成的不会是真正的随机图像。
4.stitch效果正如上面参数解释中说的那样,的确能创建平滑的边缘哦,亲!
5.fractalNoise为false的情况下生成的图像貌似一团一团的图像,用它来做烟雾(或者棉花?靠,你家楼下弹棉花的跟你很要好是不?是不是已经发展成基友关系了?)感觉挺不错。若此参数值为true,则生成的图像比较连续,若是你保持R/G/B三个ChannelOption都处于选中状态,即三个颜色都被打开着的话生成的图像就好像一堆颜色扑在一起的一个涂鸦,不过在实际应用中我们通常只开其中几个通道,不会全部通道都开着,这个稍后会有具体例子的介绍哦亲~
6.grayScale勾选则会让图像变成黑白
7.改变xOffset和yOffset会平移图像

         那么现在我们已经对于perlinNoise的参数应用有了一个比较全面的了解了,OK,让我们一起来做几个实际例子吧。事实上,在几年前我们的Ryanliu老刘大神就已经有一个使用perlinNoise做的火焰效果了(http://uh.9ria.com/link.php?url=http://bbs.9ria.com%2Fviewthread.php%3Ftid%3D26498),从这个帖子的点击率可以看出perlinNoise的魅力所在,这……这不就是吸引我们Flash编码人员入行的一个因素么?做出让人看了能够鸡动的东西,让别人夸你猛实乃人生一大快事也,我哈哈~
        众所周知的是,国外的技术发展总是比国内快很多,因此对于perlinNoise应用的探索一定也比国内多很多,我为大家找了很多不同类型的应用场合,让我们一起来享用这饕餮盛宴吧少年们!

蓝天白云效果
效果出自:http://www.flashandmath.com/intermediate/clouds/
英文好的话可以直接阅读原帖子哦亲~否则就看贫道的接下来的翻译吧……
实际效果(点击图片打开……下同,不在重复了,累了=.=):

很逼真是不是?我当初看到也颤抖了,是的,我确实颤抖了,不过幸好我定力好,没有湿。如果直接看源码可能没那么好理解,那就让我们一步步来哈,先写以下代码试验一把:

package
{
        import flash.display.Bitmap;
        import flash.display.BitmapData;
        import flash.display.Sprite;
        import flash.events.Event;
        import flash.geom.Point;
        
        public class CloudEffect extends Sprite
        {
                private var display:Sprite;
                private var perlinData:BitmapData;
                private var perlinBitmap:Bitmap;
                
                                
                private var displayWidth:Number;
                private var displayHeight:Number;

                
                private var periodX:Number;
                private var periodY:Number;
                private var seed:int;
                private var numOctaves:int;
                
                public function CloudEffect()
                {
                        init();
                }
                
                
                private function init():void {
                        
                        var i:int;
                        
                        display=new Sprite();
                        
                        this.addChild(display);
                        
                        displayWidth = stage.stageWidth;
                        displayHeight = stage.stageHeight;

                        
                        periodX=150;
                        periodY=150;
                        
                        numOctaves = 5;

                        perlinData = new BitmapData(displayWidth,displayHeight,true);
                        perlinBitmap = new Bitmap(perlinData);
                        
                        display.addChild(perlinBitmap);
                        
                        
                        seed = int(Math.random()*10000);

                         perlinData.perlinNoise(periodX,periodY,numOctaves,seed,false,true,7,false);
                }
                
        }
}

 上述代码应该不难理解,就是在舞台上添加一个bitmapData对象并为此bitmapData对象生成柏林噪声图像。最终运行结果应该如下图所示这样:

看起来又像刚才那个彩色涂鸦了,和我们的亲爱的蓝天白云效果相差甚远。没事,接下来我们要想还有哪些地方需要改进,嗯……白云看起来应该不会这么连贯,上面这个图里面多种颜色混杂,若是我去掉其中几个通道的颜色异或我只留下一个颜色通道会不会看起来像云的分布效果呢?试试看吧,把perlinNoise的第7个参数channelOption的值由默认的三通道7(这个数字是由BitmapDataChannel.RED(1) 、 BitmapDataChannel.BLUE(2)、BitmapDataChannel.GREEN(4)这三个数字通过逻辑或运算得到的 )改成一个通道1或者2或者4试试:

perlinData.perlinNoise(periodX,periodY,numOctaves,seed,false,true,1,false);

 

哈哈,效果果然如寡人所料,看起来有点云的那种分布感觉了,只不过颜色有点不对,回头想一想,出现这种结果也是应该的,因为我只开了一个红色的颜色通道。so,接下来要做的事情就是把颜色给涂成白色。说到改变像素颜色,第一个想到的工具就是ColorMatrixFilter这个滤镜,它提供的强大图片色彩改变功能在大多数情况下都能满足我们的需求(详细使用方法可参考eko大神的文章http://uh.9ria.com/link.php?url=http://bbs.9ria.com%2Fviewthread.php%3Ftid%3D47853)。
那么,我由于刚才为perlinNoise传入的第七个参数是1(BitmapDataChannel.RED),只开启了红色通道,因此我只需要在ColorMatrixFilter中设置红色通道的RGB均为255,透明度alpha为1即可,见下面源码:

        public class CloudEffect extends Sprite
        {
                ……
                private var cmf:ColorMatrixFilter;
                
                ……
                
                private function init():void {
                        
                        ……
                        
                        //将生成的杂点颜色都涂成白色,perlinNoise方法第7个参数开放的通道是哪个
                        //就对哪个通道设置为完全不透明
                        cmf = new ColorMatrixFilter([0,0,0,0,255,
                                0,0,0,0,255,
                                0,0,0,0,255,
                                1,0,0,0,0]);
                        
                        ……

                        //只开放一个颜色通道,这样就可以让我们的
                        //“云”看起来是时而零散时而连贯的
                        perlinData.perlinNoise(periodX,periodY,numOctaves,seed,false,true,1,false);
                        perlinData.applyFilter(perlinData, perlinData.rect, new Point(), cmf);

                }
                
        }
}

 我们在bitmapData生成杂点图像后为它用上一个ColorMatrixFilter将它开放的唯一一个通道的颜色染色为白色。

为了让视觉效果越发有Feeling,再给他加个蓝色的底就更好了对不对:

package
{
        
        public class CloudEffect extends Sprite
        {
                ……
                
                private var blueBackground:Shape;
                private var skyColor:uint;
                                
                ……
                
                private function init():void {
                        
                        //蓝天在我眼前
                        skyColor = 0x2255AA;
                        blueBackground = new Shape();
                        blueBackground.graphics.beginFill(skyColor);
                        blueBackground.graphics.drawRect(0,0,displayWidth,displayHeight);
                        blueBackground.graphics.endFill();
                        
                        
                        display.addChild(blueBackground);
                        display.addChild(perlinBitmap);
                                                
                        ……

                        perlinData.perlinNoise(periodX,periodY,numOctaves,seed,false,true,1,false,offsets);
                        perlinData.applyFilter(perlinData, perlinData.rect, new Point(), cmf);
                }
                
        }
}

 好了,现在应该已经看见了静态的云了。只差最后一步就是让云飘起来了。刚才在分析perlinNoise的参数的时候我们已经注意到它最后一个参数offsets,靠它应该可以平移我们的杂点图像!

那就让我们试试看在ENTER_FRAME的事件处理函数里面通过改变offsets这个perlinNoise最后一个参数的值来达到图像移动的效果。不过在动手之前我发现这个offsets参数是一个数组Array对象,为什么是一个数组呢?之前有说过,perlinNoise的第三个参数numOctaves代表了使用柏林噪声算法生成的杂点图层数量,那么由于一个perlinNoise生成的图像将是由numOctaves个杂点图层组成的,因此在移动一个perlinNoise图像的时候我们就必须明确地指定组成它的每一个杂点图层的移动目标。所以,这个offsets参数是一个长度和numOctaves值一样的的数组。看下面的源码:

public class CloudEffect extends Sprite
{
……
        private var offsets:Array;
        
        private function init():void {
                
                ……
                
                //numOctaves可以用来表示柏林图像层的数量,有几层图像就创建几个移动点
                offsets = new Array();
                for (i = 0; i<=numOctaves-1; i++) {
                        offsets.push(new Point());
                }
                
                
                this.addEventListener(Event.ENTER_FRAME, onEnter);
        }
        
        
        
        private function onEnter(evt:Event):void {
                
                var i:int;

                //移动每一层的杂点图像
                for (i = 0; i<=numOctaves-1; i++) {
                        
                        offsets[i].x += 1;
                        offsets[i].y += 0.2;
                }
                
                //只开放一个颜色通道,这样就可以让我们的
                //“云”看起来是时而零散时而连贯的
                perlinData.perlinNoise(periodX,periodY,numOctaves,seed,false,true,1,false,offsets);
                perlinData.applyFilter(perlinData, perlinData.rect, new Point(), cmf);
                
        }
}

 添加到这些代码后运行,发现云确实飘动起来了,飘动横速度是1,纵速度是0.2,你可以根据喜好来设置飘动速度和方向。不过我们发现一点就是运行这个程序的话CPU消耗率过大了,一度能冲到40以上(我是双核CPU,所以单个程序的CPU最高占有率是50),想想也是,因为每一帧都调用perlinNoise这个方法去生成图像,消耗不大才怪了。这样子可不行啊,这样还玩个P啊。当然,作者也意识到了这个效率的问题,所以他通过探索,给出了我们另一种思路来移动我们的云朵。

原文章地址:http://www.flashandmath.com/intermediate/cloudsfast/
还是那句话,英文好的直接看原文,否则就……你懂的。
      纵观我们的这个飘云的效果,我们的“云”一般只需要生成一次之后就没必要再次生成新的了,因此我们主要的工作还是在于如何让“云”飘动起来。那么既然通过频繁调用perlinNoise方法来实现云朵飘动的办法效率低下,我们就得继续寻找新的能够让图像动起来的办法。我们注意到,在BitmapData类中存在一个scroll 的方法,通过这个方法能够让一个BitmapData中的像素动起来。不过新的问题出现了,就是一旦我把整个bitmapData移动起来后那么这些像素在移动之前的位置上不就出现了像素缺口了?换句话说,(0,0)这个点原先的颜色是白色,那么当他x,y方向分别移动1像素后(1,1)点的颜色就变成了白色,那么此时(0,0)点的像素就不对了,它会保持原有的颜色白色,最终结果可能就如下图所示,拉出一条很长的轨迹来,这明显不是我们想要的结果:

那么作者利用了一种类似“传送带”的思想,用你们地球人的一句话叫做“拆东墙补西墙”,如下图所示:

上面两个插图都是引用的原文插图,前面这张插图表示进入新的一帧以后将有三部分:竖直切分块(绿色部分)、水平切分块(红色部分)以及拐角切分块(紫色部分)将会移出可视区域(假设整个图像是向右下方向移动),那么在它们移动至可视区域外之后可以补回到左上方那些新出现在可视区域内的位置,就如后面这张图所示。这样子就可以给人一种源源不断的平滑感觉,好比机场或移动扶梯的传送带。
        原理易懂实现犯难,这是很多人的通病,先上全部代码再来一点点吸收:

public class MovingClouds extends Sprite {
        
        public var numOctaves:int;
        public var skyColor:uint;
        public var cloudsHeight:int;
        public var cloudsWidth:int;
        public var periodX:Number;
        public var periodY:Number;
        /** 图像横向滚动速度 */
        public var scrollAmountX:int;
        /** 图像纵向滚动速度 */
        public var scrollAmountY:int;
        /** 最大滚动速度 */
        public var maxScrollAmount:int;
        
        private var cloudsBitmapData:BitmapData;
        private var cloudsBitmap:Bitmap;
        private var cmf:ColorMatrixFilter;
        private var blueBackground:Shape;
        private var displayWidth:Number;
        private var displayHeight:Number;
        private var seed:int;
        private var offsets:Array;
        /** 水平方向需置换的像素暂存于此 */
        private var sliceDataH:BitmapData;
        /** 垂直方向需置换的像素暂存于此 */
        private var sliceDataV:BitmapData;
        private var sliceDataCorner:BitmapData;
        private var horizCutRect:Rectangle;
        private var vertCutRect:Rectangle;
        private var cornerCutRect:Rectangle;
        private var horizPastePoint:Point;
        private var vertPastePoint:Point;
        private var cornerPastePoint:Point;
        private var origin:Point;
        private var cloudsMask:Shape;
        
        /**
         * @param w                云朵渲染矩形宽度
         * @param h                云朵渲染矩形高度
         * @param scX        云朵滚动横速度
         * @param scY        云朵滚动纵速度
         * @param useBG        是否自动绘制背景
         * @param col        背景颜色
         * 
         */                
        public function MovingClouds(w:int = 300, h:int = 200, scX:int = -1, scY:int = 2, useBG:Boolean = true, col:uint = 0x2255aa) {
                
                displayWidth = w;
                displayHeight = h;
                
                cloudsWidth = Math.floor(1.5*displayWidth);
                cloudsHeight = Math.floor(1.5*displayHeight);
                periodX = periodY = 150;
                
                scrollAmountX = scX;
                scrollAmountY = scY;
                maxScrollAmount = 50;
                
                numOctaves = 5;
                
                skyColor = col;
                        
                cloudsBitmapData = new BitmapData(cloudsWidth,cloudsHeight,true);
                cloudsBitmap = new Bitmap(cloudsBitmapData);
                        
                origin = new Point(0,0);
                
                cmf = new ColorMatrixFilter([0,0,0,0,255,
                                                                         0,0,0,0,255,
                                                                         0,0,0,0,255,
                                                                         1,0,0,0,0]);
                
                
                cloudsMask = new Shape();
                cloudsMask.graphics.beginFill(0xFFFFFF);
                cloudsMask.graphics.drawRect(0,0,displayWidth,displayHeight);
                cloudsMask.graphics.endFill();
                
                if (useBG) {
                        blueBackground = new Shape();
                        blueBackground.graphics.beginFill(skyColor);
                        blueBackground.graphics.drawRect(0,0,displayWidth,displayHeight);
                        blueBackground.graphics.endFill();
                        
                        this.addChild(blueBackground);
                }
                this.addChild(cloudsBitmap);
                this.addChild(cloudsMask);
                cloudsBitmap.mask = cloudsMask;
                                                
                makeClouds();
                setRectangles();
        
                this.addEventListener(Event.ADDED_TO_STAGE, addedToStage);
        }
        
        private function addedToStage(evt:Event):void {
                this.removeEventListener(Event.ADDED_TO_STAGE, addedToStage);
                this.addEventListener(Event.REMOVED_FROM_STAGE, removedFromStage);
                this.addEventListener(Event.ENTER_FRAME, onEnter);
        }
        
        private function removedFromStage(evt:Event):void {
                this.removeEventListener(Event.REMOVED_FROM_STAGE, removedFromStage);
                this.addEventListener(Event.ADDED_TO_STAGE, addedToStage);
                this.removeEventListener(Event.ENTER_FRAME, onEnter);
        }
        
        private function setRectangles():void {
                
                //约束滚动速度
                scrollAmountX = (scrollAmountX > maxScrollAmount) ? maxScrollAmount : ((scrollAmountX < -maxScrollAmount) ? -maxScrollAmount : scrollAmountX);
                scrollAmountY = (scrollAmountY > maxScrollAmount) ? maxScrollAmount : ((scrollAmountY < -maxScrollAmount) ? -maxScrollAmount : scrollAmountY);
                
                //创建临时存储出界像素的bitmapData
                if (scrollAmountX != 0) {
                        sliceDataV = new BitmapData(Math.abs(scrollAmountX), cloudsHeight - Math.abs(scrollAmountY), true);
                }
                if (scrollAmountY != 0) {
                        sliceDataH = new BitmapData(cloudsWidth, Math.abs(scrollAmountY), true);
                }
                if ((scrollAmountX != 0)&&(scrollAmountY != 0)) {
                        sliceDataCorner = new BitmapData(Math.abs(scrollAmountX), Math.abs(scrollAmountY), true);
                }
                
                //创建用以管理出界像素的矩形
                horizCutRect = new Rectangle(0, cloudsHeight - scrollAmountY, cloudsWidth - Math.abs(scrollAmountX), Math.abs(scrollAmountY));
                vertCutRect = new Rectangle(cloudsWidth - scrollAmountX, 0, Math.abs(scrollAmountX), cloudsHeight - Math.abs(scrollAmountY));
                cornerCutRect = new Rectangle(cloudsWidth - scrollAmountX, cloudsHeight - scrollAmountY,Math.abs(scrollAmountX), Math.abs(scrollAmountY));
                
                //创建出界像素矩形将粘贴到的新位置
                horizPastePoint = new Point(scrollAmountX, 0);
                vertPastePoint = new Point(0, scrollAmountY);
                cornerPastePoint = new Point(0, 0);
                
                //若滚动方向为向左,则将管理像素出界的矩形放在左边缘
                if (scrollAmountX < 0) {
                        cornerCutRect.x = vertCutRect.x = 0;
                        cornerPastePoint.x = vertPastePoint.x = cloudsWidth + scrollAmountX;
                        horizCutRect.x = -scrollAmountX;
                        horizPastePoint.x = 0;
                }
                //若滚动方向为向上,则将管理像素出界的矩形放在上边缘
                if (scrollAmountY < 0) {
                        cornerCutRect.y = horizCutRect.y = 0;
                        cornerPastePoint.y = horizPastePoint.y = cloudsHeight + scrollAmountY;
                        vertCutRect.y = -scrollAmountY;
                        vertPastePoint.y = 0;
                }
                
        }
        
        private function makeClouds():void {
                seed = int(Math.random()*0xFFFFFFFF);
                
                offsets = new Array();
                for (var i:int = 0; i<=numOctaves-1; i++) {
                        offsets.push(new Point());
                }
        
                cloudsBitmapData.perlinNoise(periodX,periodY,numOctaves,seed,true,true,1,true,offsets);
                cloudsBitmapData.applyFilter(cloudsBitmapData, cloudsBitmapData.rect, new Point(), cmf);
                
        }
        
        private function onEnter(evt:Event):void {
                
                //BitmapData像素级操作的常用伎俩,先锁住bitmapData让我们在未处理完全部像素前
                //不渲染此bitmapData,避免让未完成品被看见
                cloudsBitmapData.lock();
                
                //把将移除范围的像素块存起来
                if (scrollAmountX != 0) {
                        sliceDataV.copyPixels(cloudsBitmapData, vertCutRect, origin);
                }
                if (scrollAmountY != 0) {
                        sliceDataH.copyPixels(cloudsBitmapData, horizCutRect, origin);
                }
                if ((scrollAmountX != 0)&&(scrollAmountY != 0)) {
                        sliceDataCorner.copyPixels(cloudsBitmapData, cornerCutRect, origin);
                }
                
                //滚动全图
                cloudsBitmapData.scroll(scrollAmountX, scrollAmountY);
                
                //将保存了的像素块贴至新位置
                if (scrollAmountX != 0) {
                        cloudsBitmapData.copyPixels(sliceDataV, sliceDataV.rect, vertPastePoint);
                }
                if (scrollAmountY != 0) {
                        cloudsBitmapData.copyPixels(sliceDataH, sliceDataH.rect, horizPastePoint);
                }
                if ((scrollAmountX != 0)&&(scrollAmountY != 0)) {
                        cloudsBitmapData.copyPixels(sliceDataCorner, sliceDataCorner.rect, cornerPastePoint);
                }
                
                cloudsBitmapData.unlock();
        }


}

 如果你直接看原文给出的源码,估计你很难理解,我这里加了一点注释帮助理解。作者现在将云朵对象封装成了一个单独的类,允许我们在创建云朵的时候能够传入一些设置参数,如云朵的渲染尺寸以及移动速度等。所谓的“渲染尺寸”是我们能够看见的云朵尺寸,实际的云朵尺寸应该比这个尺寸还要大一些,这里就用到了之前我用过多次的遮罩(mask)的功能,如下图:

使用一个矩形遮罩可以让一个图像只有中间一部分能被看到,那么这样就不会让我们在后台做“像素裁剪”这种事情的时候不容易被用户意识到,如图中的红球。在上面这段代码中,原作者将云朵的实际大小设置为渲染尺寸的1.5倍大。之后,我们注意到,他只调用了一次makeCloud方法,这正如之前所说,我们的云朵只需要创建一次就够了,不需要创建多次,一旦创建后我们需要做的就是调用setRectangles方法来定义我们每帧都要裁剪的像素区域。在setRectangles方法中的代码是我们需要额外用心去研究的,首先,将每帧需要移动的像素值(即移动速度)限制在-scrollAmountX/Y和scrollAmountX/Y之间,之后,按照上面给出的示意图进行那三块像素矩形的创建。为了防止在裁剪过程中出现像素遗失,需要一个第三方bitmapData对象来暂存我们剪下来的像素块。
      最后,侦听ENTER_FRAME事件并在onEnter事件处理函数里面进行像素裁剪工作,遵循的顺序是:剪—> 存入第三方bitmapData对象 —> 滚动全图 —> 将之前剪下像素贴到滚动后留下的像素缺口。
       看完了源码,我们就可以在文档类中很容易地创建一个MovingClouds对象了:

public class OptimizedCloudEffect extends Sprite
{
        public function OptimizedCloudEffect()
        {
                super();
                
                var clouds:MovingClouds = new MovingClouds(500,380, -2, 1, true);
                addChild( clouds );
        }
}

 运行结果和之前讲得一样,但是感觉流畅很多,CPU使用率不足10,非常不错的表现……

为了让结果更加真实,我们可以创建叠层云,即两个或多个MovingClouds对象,这样有一种层次感:

public class OptimizedCloudEffect extends Sprite
{
        public function OptimizedCloudEffect()
        {
                super();

                //叠层云
                var clouds1:MovingClouds = new MovingClouds(500,380,-2,3,false);
                clouds1.alpha = 0.4;
                
                var clouds2:MovingClouds = new MovingClouds(500,380,-1,1,false);
                clouds2.alpha = 1;
                
                var blueBackground:Shape = new Shape();
                blueBackground.graphics.beginFill(0x1E4A95);
                blueBackground.graphics.drawRect(0,0,500,380);
                blueBackground.graphics.endFill();

                addChild(blueBackground);
                addChild(clouds1);
                addChild(clouds2);
        }
}

 叠层云效果如下:

类似地,如果你把云朵颜色改成黑色,天空底色改成暗蓝色,就可以变成夜晚的天空,你甚至还可以添加一个Shape作为月亮,演示效果就不看了哈^_^

原文地址:https://www.cnblogs.com/keng333/p/3195486.html