2009년 봄, 일본에서 ActionScript 개발자들간에 재미있는 실험이 있었다. 그것은 "Flash속도를 극대화하기" 실험중 하나로 수만개의 입자를 가지고 가상의 유체 시뮬레이션을 보여주는 예제를 만드는 것이였다.

수만개의 입자를 이용한 유체 시뮬레이션 실행


위 화면은 최초실험대상이 된 1만개 입자 유체 시뮬레이션 코드의 실행화면이다.

아래 소스는 위에서 소개된 소스를 10만개 입자로 수정하고 최대 frameRate를 110으로 조정한 것이다. 또한 마우스 클릭으로 초기화 하는 대신 500ms단위로 유체의 이동패턴과 전체색이 조금씩 변하도록 수정했다.

package {
	import flash.display.*;
	import flash.events.*;
	import flash.geom.*;
	import flash.utils.*;
	
	import net.hires.debug.Stats;
	
	[SWF(width="465", height="465", backgroundColor="0x000000", frameRate="110")];
	/**
	 * BitmapData를 이용한 파티클 렌더링 속도 테스트 
	 * @see http://clockmaker.jp/blog/2009/04/particle/
	 */ 
	public class bitmap_liquid100000 extends Sprite {
		
		private const nums:uint=100000;
		private var bmpDat:BitmapData;
		private var vectorDat:BitmapData;
		private var randomSeed:uint;
		private var bmp:Bitmap;
		private var vectorList:Array;
		private var rect:Rectangle;
		private var cTra:ColorTransform;
		private var vR:Number;
		private var vG:Number;		
		private var timer:Timer;
		
		public function bitmap_liquid100000() {
			initialize();
		}
		
		private function initialize():void {
			//stage 관련의 설정
			stage.align=StageAlign.TOP_LEFT;
			stage.scaleMode=StageScaleMode.NO_SCALE;
			stage.frameRate=110;
			
			//파티클이 렌더링 되는 메인 Bitmap. 
			bmpDat=new BitmapData(465, 465, false, 0x000000);
			bmp=new Bitmap(bmpDat);
			addChild(bmp);
			
			//파티클의 가속도를 계산하기 위한 도움 BitmapData로서 perlinNoise가 적용된다. 
			vectorDat=new BitmapData(465, 465, false, 0x000000);
			randomSeed=Math.floor(Math.random() * 0xFFFF);
			vectorDat.perlinNoise(230, 230, 4, randomSeed, false, true, 1 | 2 | 0 | 0);
			//addChild(new Bitmap(vectorDat)); //만약 이 perlinNoise가 적용된 Bitmap을 보고 싶다면 주석을 풀자 
			
			//화면크기 
			rect=new Rectangle(0, 0, 465, 465);
			
			//파티클 궤적을 그리기 위함 
			cTra=new ColorTransform(.8, .8, .9, 1.0);
			vR = 0;
			vG = 0;
			
			//파티클을 넣기 위한 List
			vectorList=new Array();
			for (var i:uint=0; i < nums; i++) {
				//파티클 위치
				var px:Number=Math.random() * 465;
				var py:Number=Math.random() * 465;
				var pv:Point=new Point(px, py);
				//파티클 가속도
				var av:Point=new Point(0, 0);
				//파티클 속도 
				var vv:Point=new Point(0, 0);
				//파티클 위치,가속도,속도 정보를 List에 저장 
				var hoge:VectorDat=new VectorDat(av, vv, pv);
				vectorList.push(hoge);
			}
			
			//지속적인 파티클 렌더링을 위한 loop 함수 호출 
			addEventListener(Event.ENTER_FRAME, loop);
			
			//500ms마다 
			timer = new Timer(500, 0);
			timer.addEventListener(TimerEvent.TIMER, resetFunc);
			timer.start();
			
			//통계 
			addChild(new Stats);
		}
		
		private function loop(e:Event):void {
			//렌더링용 BitmapData를 colorTransform로 어둡게 하여 기존에 그려진 파티클의 궤적을 보이도록 함 
			bmpDat.colorTransform(rect, cTra);
			
			//파티클의 위치를 재계산하여 렌더링한다.
			var list:Array=vectorList;
			var len:uint=list.length;
			for (var i:uint=0; i < len; i++) {
				var dots:VectorDat=list[i];
				var col:Number=vectorDat.getPixel(dots.pv.x, dots.pv.y);
				var r:uint=col >> 16 & 0xff;
				var g:uint=col >> 8 & 0xff;
				dots.av.x+=(r - 128) * .0005; //적색을 x축 가속도로 사용
				dots.av.y+=(g - 128) * .0005; //녹색을 y축 가속도로 사용
				dots.vv.x+=dots.av.x;
				dots.vv.y+=dots.av.y;
				dots.pv.x+=dots.vv.x;
				dots.pv.y+=dots.vv.y;
				
				var _posX:Number=dots.pv.x;
				var _posY:Number=dots.pv.y;
				
				dots.av.x*=.96;
				dots.av.y*=.96;
				dots.vv.x*=.92;
				dots.vv.y*=.92;
				
				//stage 밖으로 이동했을 경우 처리. 3항 연산자 처리함 
				(_posX > 465) ? dots.pv.x=0 : (_posX < 0) ? dots.pv.x=465 : 0;
				(_posY > 465) ? dots.pv.y=0 : (_posY < 0) ? dots.pv.y=465 : 0;
				
				//1*1 pixel을 bitmapData에 렌더링 
				bmpDat.fillRect(new Rectangle(dots.pv.x, dots.pv.y, 1, 1), 0xFFFFFF);
			}
		}
		
		private var seed:Number = Math.floor( Math.random() * 0xFFFF );
		private var offset:Array = [new Point(), new Point()];
		private function resetFunc(e:Event) :void{
			//파티클의 가속도를 계산하기 위한 도움 BitmapData로서 perlinNoise를 변경 
			vectorDat.perlinNoise( 230, 230, 3, seed, false, true, 1|2|0|0, false, offset );
			offset[0].x += 20;
			offset[1].y += 20;
			
			//파티클 궤적을 표시하기 위한 부분을 변경  (조금씩 색변동이 일어난다)
			var dots:VectorDat = vectorList[0];
			vR += .001 * (dots.pv.x-232)/465;
			vG += .001 * (dots.pv.y-232)/465;
			( vR > .01 ) ? vR = .01:
				( vR < -.01 ) ? vR = -.01:0;
			( vG > .01 ) ? vG = .01:
				( vG < -.01 ) ? vG = -.01:0;
			
			cTra.redMultiplier += vR;
			cTra.blueMultiplier += vG;
			( cTra.redMultiplier > .9 ) ? cTra.redMultiplier = .9:
				( cTra.redMultiplier < .5 ) ? cTra.redMultiplier = .5:cTra.redMultiplier;
			( cTra.blueMultiplier > .9 ) ? cTra.blueMultiplier = .9:
				( cTra.blueMultiplier < .5 ) ? cTra.blueMultiplier = .5:cTra.blueMultiplier;         
		}
	}
}

import flash.geom.Point;

class VectorDat {
	public var vv:Point;
	public var av:Point;
	public var pv:Point;
	
	function VectorDat(_av:Point, _vv:Point, _pv:Point) {
		vv=_vv;
		av=_av;
		pv=_pv;
	}
}

처음 이 소스를 본다면 뭔가 굉장히 복잡할 것이라고 생각할지 모른다. 지금껏 이런 것을 구현하기 위해 ActionScript 3.0의 DisplayObject객체 기반으로 입자를 구현하는 것을 먼저 생각한 분이라면 위 소스가 더 어렵게 느껴질 것이다. 참고로 만약 10만개의 입자를 전부 DisplayObject 객체로 구현하면 Flash Player는 제대로 작동도 못하고 중지될 것이다. DisplayObject와 그것을 확장한 화면 표시 객체는 다소 무겁다. Flash Player가 이런 무거운 객체를 10만개나 frameRate가 24 수준으로 그리는 것은 일반 보급 컴퓨터에서는 불가능하다.

위 소스를 잘보면 알겠지만 입자를 표현하기 위해 BitmapData(bmpDat변수 참고)를 이용한다. BitmapData 하나에 모든 입자 정보가 표현되도록 하는 것이 첫번째 아이디어이다. 이렇게 함으로써 10만개 입자 렌더링이 가능해진다.

처음 실행하면 무작위로 지정된 위치에 입자들이 배치되었다가 무작위 궤적을 따라 이동하는 것을 볼 수 있다. 이러한 이동이 뭔가 엄청나게 고난이도 물리엔진  알고리즘이 들어간 것 같지만 실상은 전혀 아니다. 입자의 위치를 이용해 가속도와 속도를 계산하는데 사용된 것은 다름 아닌 BitmapData(vectorDat변수 참고)이다. 이 BitmapData는 실제화면에는 눈에 보이지 않는다.  대신 가속도와 속도를 계산하기 위해 BitmapData에 PerlinNoise를 주어 입자의 위치정보에 해당하는 색정보를 얻어 색의 RGB값중 R값은 x축 가속도값으로 G값은 y축 가속도 값으로 참조하도록 구현되어 있다. PerlinNoise가 적용된 BitmapData는 아래와 같다. 실제로 이 화면을 보고 싶다면 위 코드의  "//addChild(new Bitmap(vectorDat))" 부분을 찾아 주석처리를 삭제하고 실행해 보면 된다.  

필자는 개인적으로 PerlinNoise 대신이 뭔가 물리학적 정보가 들어간다면 훨씬더 실용성 있는 재미난 교육용 자재로 만들 수 있지 않을까 생각했다.

입자의 가속도 계산을 위한 PerlinNoise가 적용된 BitmapData의 모습. 500ms단위로 계속 변한다.


BitmapData에 입자들의 위치를 갱신하는 것까지는 알겠는데... 입자들이 특정한 궤적을 남기도록 하는 효과는 도데체 어떻게 하는 것일가? 따로 궤적을 다루는 Array나 List가 있는 것일까? 아니다. ColorTransform 클래스에 비밀이 숨겨져 있다. loop() 함수를 보면 bmpDat.colorTransform(rect, cTra); 부분이 있다 .rect은 colorTransform을 적용할 범위이고 cTra가 적용할 ColorTransform객체이다. 이 객체에는 cTra.redMultiplier, cTra.greenMultiplier, cTra.blueMultiplier 속성등이 있다. 이 값은 기존 BitmapData에 RGB값을 해당값을 정해진 값으로 곱해주는 역할을 하는데 가령 cTra.redMultiplier가 0.8이면 loop를 돌때마가 Red값이 0.8배로 감소하게 된다. 결국 입자의 흰색(0xffffff)로 BitmapData에 찍히면 cTra.redMultiplier에 의해 계속 어두워지다가 결국 검은색이 된다. 이러한 원리로 궤적이 남게 되는거다.

하지만 위 코드는 문제가 있다. 실제로 실행해보면 필자의 컴퓨터에서는 frameRate가 10도 안나왔다. 최대치 110을 주었음에도 이 정도 밖에 안나왔다는 것에 대해 개발자로서 계산 및 렌더링 최적화에 관심을 자연스럽게 가지게 된다.

일본 개발자들은 Wonderfl의 "Fork"기능을 이용해 서로 소스를 공유하며 위 코드의 문제를 계속 개선해 나갔다. 

아래 압축파일은 필자가 여러 단계를 거쳐서 실험했던 코드를 압축한 것이다.



개선사항 1. BitmapData의 lock()과 unlock() 함수 추가 (bitmap_liquid100000_01.as)
BitmapData가 Bitmap을 통해 화면에 이미 렌더링 자원으로 활용되고 있다면 BitmapData가 수정될 때 마다 Bitmap에도 갱신한다.(즉, addChild(new Bitmap(bitmapData))일때를 의미한다.)  구체적으로 설명하자면 입자가 10만개 이므로 입자의 위치가 바뀔때마다 10만번의 BitmapData를 수정하면 그때마다 Bitmap에도 영향을 준다. 만약 BitmapData을 수정하고 난 다음에 마지막에 Bitmap에 적용될 수 있도록 하면 속도개선에 도움을 줄 것이다. 이것을 가능하게 하는 함수가 BitmapData의 lock(), unlock()함수이다. 위 코드에서 loop()함수에 맨처음과 맨끝부분에 각각 bmpDat.lock(), bmpDat.unlock()을 추가하자. 이 작업으로 전체적으로 2~3 frameRate 개선을 볼 수 있었다. frameRate가 이 실험에서는 크게 개선되지 않았지만 만약 BitmapData에 이보다 더욱 복잡한 수정이 있거나 더 크기가 크면 클수록 그 차이가 더 할 것이다.
private function loop(e:Event):void {
	bmpDat.lock();
	(생략)
	bmpDat.unlock();
}



개선사항 2. fillRect을 setPixel로 변경 (bitmap_liquid100000_02.as)
위 코드에서 가장 잘못된 부분중에 하나가 1px짜리 입자를 렌더링하기 위해 fillRect()을 이용했다는 것이다. 이 함수 대신 setPixel()로 수정한다. 이 작업으로 25~30 frameRate 개선을 했다. 여기까지만 하더라도 상당한 진전이다.

개선사항 3. point 대신 number로 (bitmap_liquid100000_03.as)
위 코드에서 비효율적인 코드부분이 있다. 바로 입자의 정보를 담은 VectorDat부분이다.

import flash.geom.Point;

class VectorDat {
	public var vv:Point; //속도
	public var av:Point; //가속도
	public var pv:Point; //위치 
	
	function VectorDat(_av:Point, _vv:Point, _pv:Point) {
		vv=_vv;
		av=_av;
		pv=_pv;
	}
}

위 클래스는 입자의 위치,속도,가속도 정보를 Point를 이용해 만들었다. 이렇게 하게 되면 10만개나 되는 입자의 위치를 vectorDat.pv.x, vectorDat.pv.y 형태로 접근해야 한다. 이것 대신 vectorDat.px, vectorDat.py로 하면 훨씬 빠른 접근이 가능해질 것이다. 반복되는 로직에 깊은 DOM을 접근을 사용하면 그만큼 느려진다는 것을 항상 염두할 필요가 있다. 다음 코드는 위 클래스를 개선한 것인다.

class VectorDat {
	public var vx:Number = 0; //x축 속도
	public var vy:Number = 0; //y축 속도
	public var ax:Number = 0; //x축 가속도
	public var ay:Number = 0; //y축 가속도
	public var px:Number; //x축 위치 
	public var py:Number; //y축 위치 
	
	function VectorDat( px:Number, py:Number ) {
		this.px = px;
		this.py = py;
	}
}

위 클래스에 맞게 전체 소스를 수정한 뒤 실행하면 frameRate가 10이상 개선되는 것을 확인할 수 있었다. 꽤 큰 진전이다.


개선사항 4. 가속도 계산을 위한 bitmapData 크기 조정 (bitmap_liquid100000_06.as)

BitmapData의 getPixel은 BitmapData의 크기가 크면 클수록 그 효율이 떨어진다. 위 코드에서 입자의 가속도를 계산하기 위해 perlinNoise를 적용한 BitmapData(vectorDat)는 렌더링하기 위한 BitmapData(bmpDat)와 크기가 동일하다. 이 크기를 465x465 대신 28x28로 줄이고 loop()함수내에 col=vectorDat.getPixel(dots.px, dots.py)대신 col=vectorDat.getPixel(dots.px>>4, dots.py>>4)로 수정해서 테스트하면 frameRate가 위 개선사항을 모두 적용한 것보다 약 10이상 증가한다. 



지금까지 개선사항 4가지를 통해 최초 코드의 frameRate를 12에서 60~70까지 끌어올릴 수 있었다. 아래는 실행화면이다.

실행 화면


결과 보러 가기 : http://wonderfl.net/code/2e15897ec742a2e2c09255bcba35c2b20a026356


다른 실험들...

필자는 VectorDat를 담는 List의 형태를 Array에서 Vector로 바꿔서 실험을 해봤다. 이 부분에 있어서는 거의 frameRate변화가 없었다. (bitmap_liquid100000_04.as참고)

또한 List형태를 Linked List로 바꿔봤는데.. 오히려 frameRate가 감소했다.  (bitmap_liquid100000_05.as참고)

마지막으로 BitmapData의 setPixel()보다 setVector()가 더 빠르다기에 그것으로 실험해 보았다. 하지만 이 방법은 위 실험에 쓰기에 적합하지 않았다. 왜냐하면 단순히 setPixel()과 setVector()만 놓고 보았을때 setVector()가 더 빠르지만 setVector()를 사용하기 위해서는 getVector()와  같은 부가적인 코드가 더 들어가게 되었고 그 코드들로 인해 오히려 frameRate가 더 줄어들었다. 그러므로 뭔가 도입할때는 그 상황에 맞게 도입해야한다. (bitmap_liquid100000_07.as참고)


추가사항 



웹눈님께서 이글을 보고 위 프로그램을 만들었는데 mouseX, mouseY 엑세스 비용문제를 해결하지 못해 속도 저하가 일어났었다.

while(pt.next) {
   var dx:Number = pt.x - mouseX;
   var dy:Number = pt.y - mouseY;
   (중간생략)
   pt = pt.next;
}


위처럼 만드셨는데...이렇게 하면 입자(파티클)이 10만개면 10번 mouseX, mouseY를 참조하게 된다. 이 속성을 엑세스 하는 것은 꽤 비싼 자원이 소비되므로 아래처럼 하면 속도개선 된다.

var mx:Number, my:Number;
mx = mouseX;
my = mouseY;
while(pt.next) {
   var dx:Number = pt.x - mx;
   var dy:Number = pt.y - my;
   (중간생략)
   pt = pt.next;
}


테스트 결과, 저렇게 바꾸는 것만으로 FPS(frame per seconds)가 무려 40에서 90까지 개선되었다.
별거 없는데도 엄청난 속도 개선이다! 


정리하며

일본에서 진행된 이 입자 유체 시뮬레이션 실험은 꽤 인기를 얻었으며 Wonderfl 사이트의 favorite와 fork기능을 통해 계속 퍼져나갔다. 필자도 예전부터 봐왔던 실험이였으나 BitmapData 활용을  이미지 에디터 정도의 프로젝트 외에는 거의 사용을 하지 않아서 깊숙히 파고들지 않았다. 

이런 단순한 실험이 일본의 여러 Flash 개발자들 사이에 인기를 끌고 함께 코드를 수정하는 모습을 보면서 그들의 개발분위기가 무척 부러웠다. 한국에도 이런 분위기가 정착되길 바란다. 

필자는 지금도 Wonderfl을 하루에도 수십번 방문한다.


참고글
Wonderfl - build Flash online
빠른 flash 연구회를 wonderfl에서
Flash 10, Massive amounts of 3D particles with Alchemy (source included).
Massive amounts of 3D particles without Alchemy and PixelBender
Flash 10, Massive amounts of 3D Particles (with haXe)
Using BitmapData.setVector for better performance


글쓴이 : 지돌스타(http://blog.jidolstar.com/670
 

+ Recent posts