[Flash] Molehill을 이용한 2D 렌더링

2011.04.21 13:54
지난 2011년 2월말에 Flash Player 11 Incubator 버전이 배포됨에 따라서 3D GPU 가속 렌더링을 위한 Molehill을 미리 경험할 수 있게 되었다. 하지만 Molehill의 등장은 반드시 3D GPU 가속 및 3D 표현 향상만을 의미하지 않는다. 이 관점은 3D 렌더링 엔진을 이용해 2D 렌더링 형태로 활용해도 된다는 관점에서 이해될 수 있다. 기존(현재) Flash Player 10 이하에서 렌더링은 2D를 표현하기 위해 CPU를 이용한 계산으로 렌더링을 하지만 Molehill을 이용하면 이것 조차 GPU를 적용할 수 있으니 기존 2D 게임을 Molehill버전으로 바꿔주면 더욱 쾌적하고 빠른 게임이 될 수 있다.

이 글의 목적은 Molehill을 이용한 2D 그래픽 GPU 가속에 대한 내용을 소개하는데 있다. 그럼 먼저 Molehill에 대해서 간단하게 소개하고 2D 렌더링 속도에 어떤 장점이 있는지 살펴보도록 하겠다.

참고로 아래 글을 통해 Molehill관련 데모와 개발환경 구축방법을 볼 수 있겠다.

Adobe Flash Player 11 Incubator의 "Molehill" 3D를 활용하기 위한 개발환경 만들기http://blog.jidolstar.com/759 



1. Molehill을 이해하기
Flash Player 10.1 이후로 3D API가 공개되었다. 이것은 flash.display.Stage위에 flash.display.DisplayObject기반에서 3D를 다룰 수 있는 API를 추가적으로 제공해준 형태이다. 하지만 완벽한 GPU렌더링을 하지 않으며 z-sort, clipping등과 같은 3D 렌더링을 위한 주요 기능은 전부 배제되었기 때문에 몇몇사람들은 2.5D라고 불렀다.

Molehill에서 3D는 flash.display.Stage위에서 렌더링되는 것이 아니다. 전혀 다른 영역에서 그려지며 flash.display.Stage와는 철저하게 물리적으로 분리되어 있다. 즉 DisplayObject기반으로 돌아가는 것이 아니며 그것과 전혀 연관없는 다른 stage위에서 동작한다. 이와 비슷한 개념은 본인의 블로그의 StageVideo 개념에 대해서 설명하면서 이야기 한바 있다.(StageVideo는 이미 현재 공식배포중인 Flash Player 10.2 기반에서 동작되어지는 부분이다.) StageVideo처럼 별도의 영역에서 렌더링 되어진다는 것이 Molehill에서 지원하는 GPU가속의 비밀(?)인 것이다. 아래 링크를 통해 StageVideo에 대해서 이해할 수 있다.

Flash Player 10.2의 StageVideo에 대해http://blog.jidolstar.com/743

Molehill의 렌더링 영역과 기존 Stage 렌더링 영역이 물리적으로 분리되어 있다는 것은 결국 Molehill만의 별도의 API가 있다는 것을 의미한다. 구체적으로 flash.display.Stage 대신 flash.display.Stage3D 가 존재한다.


위 그림은 Stage3D의 Stage의 관계를 묘사하고 있다. Flash 2D graphics는 Stage 영역을 말하며 Stage3D[0], Stage3D[1]은 Stage3D 영역이다. Stage3D는 Stage위에 올라올 수 없으며 항상 아래에 있다. Stage가 투명인 경우에 Stage3D를 보여줄 수 있다는 점을 말하고 있다. Stage3D는 Stage속성의 stage3Ds.<Vector>를 참고하여 stage.stage3Ds[0] 형태로 접근이 가능하다. 이와는 별도로 StageVideo는 Stage3D 아래에 위치한다. Stage위에 존재할 수 있는 DisplayObject는 Stage3D위에 올라올 수 없다는 점도 기억해둘 필요 있다.

아래 그림은 Stage3D API문서의 일부분을 캡쳐한 것이다.



Stage3D는 flash.display3d.Context3D의 렌더링 영역이며 Stage3D로부터 Context3D를 참조할 수 있다.  Context3D는 일종의 메모리 공간으로 3D 객체를 다루는 데이터 영역이다. (Stage3D와 Context3D의 관계를 이해하기 위해 Bitmap과 BitmapData와의 관계처럼 해석해도 무방할 듯 싶다.) Context3D에서는 z-buffer, back buffer, Color, blend mode, pixel 및 vertex shader 프로그램, vertex stream, vertext indeices, texture 등과 같은 다양한 3D 기능이 담겨있다. 즉 진정한 3D 기능이 모두 주어진 셈이다. 참고로 아래그림은 flash.display3D 패키지에 포함되어있는 Context3D 관련 클래스들이다. 



Molehill API에 익숙해지기 위해 한글문서만큼 좋은 것은 없을 것이다. 아직 초반 기술이라 Adobe에서 제공하는 별도의 한글문서가 없지만 검색해보면 응용형태로 API를 이해해 볼 수 있도록 정리한 고마운 글도 있으니 참고바란다.

Molehill Getting Started :  http://blog.naver.com/q3korea/120126463667


위 문서에 있는 코드를 보면서 느끼겠지만 Molehill API는 매우 저수준의 언어로 구성되어 있다. 결국 학습하기가 어려울 뿐아니라 코딩하는 양도 만만치 않다. 이 때문에 Molehill API를 고수준의 언어로 랩핑(wapping)해준 새로운 3D 라이브러리를 찾기 마련이다. 이미 Away3D, Alternative3D와 같은 유명한 Flash 3D 라이브러리는 기존 Flash Player 9, 10 기반에서 제공하는 API의 인터페이스를 크게 변경하지 않고 Molehill을 경험할 수 있도록 제공하고 있다.


2. 기존 방식과 Molehill과의 2D 렌더링 속도 비교

제목에서 기존방식이라 함은 flash.display.Stage 기반의 렌더링 방식을 의미한다. 기존방식의 렌더링을 위해서는 GPU 도움을 받기 어렵다. 결국 이 기반에서 수많은 렌더링 대상 객체의 수는 제한적일 수 밖에 없었다. 물론 그 안에서도 적절한 최적화를 통해 훌륭한 게임이 많이 나올 수 있었지만 제약은 분명히 있었다.

Molehill을 2D로 표현한다는 것은 실제로는 3D지만 2D처럼 이용한다는 말과 동일하다. 


위 그림은 3D 상에서 사각형을 표현하기 위한 방법을 묘사하고 있다. 기본적으로 3D 객체는 Vertext 3개로 구성된 삼각형 모양의 조각들의 조합으로 이뤄진다. 위처럼 사각형을 하나 만든다면 삼각형 두 조각이 필요한 샘이다. 두개의 삼각형이 한개의 사각형을 이룬다는 점은 하나의 2D 객체 한개를 표현하기 위한 방법으로 사각형 하나면 되고 화면의 시선방향에 대해 사각형의 면이 직각을 이루면 그것이 2D인 것이다. 표현하는 방법은 약간 복잡하더라도 GPU 가속의 큰 도움을 받을 수 있기 때문에 그 정도는 감수할 수 있다.

2.1 기존방식 예제
작년에 필자는 Flash 어플의 속도 개선을 위한 다양한 실험을 했었다. 

Flash 속도 개선을 위한 실험 - 10만개 입자 유체 시뮬레이션 연장전!http://blog.jidolstar.com/671

어떤 언어든 당연한 이야기 이겠지만 ActionScript 3.0으로 만들어지는 코드 한줄 한줄이 Flash 어플 속도에 얼마나 영향을 미칠 수 있는가 알 수 있다. 이외에도 계산이든 렌더링이든지 속도를 향상시킬 수 있는 방법은 여전히 많다. 가령 Pixel Bender, Shader 등을 이용해서 더 빠른 속도를 만들어 낼 수 있다. 하지만 그것도 기존에 나와 있는 다양한 3D 게임들을 따라올법만한 속도는 아닐것이다. 

아래에 위 실험을 통해 사용된 코드를 볼 수 있다.

이 코드를 컴파일 하기 위해 아래 라이브러리 및 클래스가 미리 준비되어야 한다.

com.adobe.utils.AGALMiniAssembler : http://goo.gl/ITcrn 
net.hires.debug.Stats : http://goo.gl/EgZ1s


//출처 : http://wonderfl.net/c/c0Gi/read , http://clockmaker.jp
package 
{
	/**
	 * 수많은 화살표 데모
	 * 고속화를 위한 테스트 
	 * 화살표 1000개 
	 * @author Yasu
	 */
	import flash.display.*;
	import flash.events.*;
	import flash.geom.*;
	import flash.utils.*;
	
	import net.hires.debug.Stats;
	
	[SWF(width="465", height="465", backgroundColor="0xFFFFFF")]
	public class NoMolehillTest extends Sprite {
		private const NUM_PARTICLE:uint = 1000;
		private const ROT_STEPS:int = 120;
		
		private var forceMap:BitmapData = new BitmapData( 233, 233, false, 0x000000 );
		private var randomSeed:uint = Math.floor( Math.random() * 0xFFFF );
		private var particleList:Vector.<Arrow> = new Vector.<Arrow>(NUM_PARTICLE, true);
		private var rect:Rectangle = new Rectangle( 0, 0, 465, 465 );
		private var seed:Number = Math.floor( Math.random() * 0xFFFF );
		private var offset:Array = [new Point(), new Point()];
		private var timer:Timer;
		private var world:Sprite = new Sprite();
		private var rotArr:Vector.<BitmapData> = new Vector.<BitmapData>(ROT_STEPS, true);
		
		public function NoMolehillTest() {
			
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.quality = StageQuality.BEST;
			stage.frameRate = 110;
			
			addChild(world);
			
			// 포스 맵의 초기화를 행합니다
			resetFunc();
			
			// 루프 처리
			addEventListener( Event.ENTER_FRAME, loop );
			
			//시간차이로 포스(force)맵과 색변화의 상태를 변경할 것임
			var timer:Timer = new Timer(1000)
			timer.addEventListener(TimerEvent.TIMER, resetFunc);
			timer.start();
			
			//화살표를 생성
			var dummy:Sprite = new Sprite();
			dummy.graphics.beginFill(0xFFFFFF, 1);
			dummy.graphics.lineStyle(1, 0x0, 1);
			dummy.graphics.moveTo(2, 4);
			dummy.graphics.lineTo(8, 4);
			dummy.graphics.lineTo(8, 0);
			dummy.graphics.lineTo(20, 7);
			dummy.graphics.lineTo(8, 14);
			dummy.graphics.lineTo(8, 10);
			dummy.graphics.lineTo(2, 10);
			dummy.graphics.lineTo(2, 4);
			
			var matrix:Matrix;
			var i:int = ROT_STEPS;
			while (i--)
			{
				matrix = new Matrix();
				matrix.translate( -11, -11);
				matrix.rotate( ( 360 / ROT_STEPS * i )* Math.PI / 180);
				matrix.translate( 11, 11);
				rotArr[i] = new BitmapData(22, 22, true, 0x0);
				rotArr[i].draw(dummy, matrix);
			}
			
			// 파티클을 생성
			for (i = 0; i < NUM_PARTICLE; i++) {
				var px:Number = Math.random() * 465;
				var py:Number = Math.random() * 465;
				particleList[i] = new Arrow(px, py);
				world.addChild(particleList[i]);
			}
			
			// 상태표시 
			addChild(new Stats);
		}
		
		private function loop( e:Event ):void {
			
			var len:uint = particleList.length;
			var col:Number;
			
			for (var i:uint = 0; i < len; i++) {
				
				var arrow:Arrow = particleList[i];
				
				var oldX:Number = arrow.x;
				var oldY:Number = arrow.y;
				
				col = forceMap.getPixel( arrow.x >> 1, arrow.y >> 1);
				arrow.ax += ( (col      & 0xff) - 0x80 ) * .0005;
				arrow.ay += ( (col >> 8 & 0xff) - 0x80 ) * .0005;
				arrow.vx += arrow.ax;
				arrow.vy += arrow.ay;
				arrow.x += arrow.vx;
				arrow.y += arrow.vy;
				
				var _posX:Number = arrow.x;
				var _posY:Number = arrow.y;
				
				var rot:Number = - Math.atan2((_posX - oldX), (_posY - oldY)) * 180 / Math.PI + 90;
				var angle:int = rot / 360 * ROT_STEPS | 0;
				// Math.abs보다 더 빠른 계산을 위해 
				angle = (angle ^ (angle >> 31)) - (angle >> 31);
				arrow.rot += (angle - arrow.rot) * 0.2;
				arrow.bitmapData = rotArr[arrow.rot];
				
				arrow.ax *= .96;
				arrow.ay *= .96;
				arrow.vx *= .92;
				arrow.vy *= .92;
				
				// 좌표 배치 좌표를 정수화해 둡니다
				arrow.x = arrow.x | 0;
				arrow.y = arrow.y | 0;
				
				( _posX > 465 ) ? arrow.x = 0 :
					( _posX < 0 ) ? arrow.x = 465 : 0;
				( _posY > 465 ) ? arrow.y = 0 :
					( _posY < 0 ) ? arrow.y = 465 : 0;
			}
		}
		
		private function resetFunc(e:Event = null):void{
			forceMap.perlinNoise(117, 117, 3, seed, false, true, 6, false, offset);
			
			offset[0].x += 1.5;
			offset[1].y += 1;
			seed = Math.floor( Math.random() * 0xFFFFFF );
		}
	}
}

import flash.display.*;

class Arrow extends Bitmap
{
	public var rot:int = 0;
	public var vx:Number = 0;
	public var vy:Number = 0;
	public var ax:Number = 0;
	public var ay:Number = 0;
	
	function Arrow( x:Number, y:Number) {
		this.x = x;
		this.y = y;
	}
}

위 코드는 아래 링크에서 실행해 볼 수 있다. 
http://wonderfl.net/c/c0Gi/read


아래 화면은 위 코드를 실행시에 CPU 사용율과 함께 스크린 캡쳐를 한 것이다.



1000개의 화살표를 렌더링하는데 40~50 FPS 정도의 속도를 유지하면서 CPU 사용율이 70~80 정도 되었다.(필자 PC 스펙이 너무 구형이라 그럴 수도... ㅡㅡ) 분명 속도 자체는 문제 없겠지만 CPU 사용율이 너무 커서 다른 업무에 방해가 될 수 있다. 결국 CPU 사용율을 줄이기 위해 stage.frameRate를 20~30으로 줄일 수 밖에 없다. 또는 성능에 따른 렌더링 주기 조정(http://diebuster.com/flash/170)을 통해 CPU 점유율을 줄일 수 있다. 



2.2 Molehill을 이용한 2D 렌더링 예제

기존방식으로 렌더링하는 것은 속도 개선 및 CPU 사용율 줄이기에 대한 노하우가 크게 필요하다는 사실을 알았다. 물론 Molehill을 쓴다고 이런 노하우는 필요하다. 그래서 같은 방법으로 노하우를 적용하되 Molehill을 함께 사용한다면 사용자의 경험을 더욱 극대화 시킬 수 있을 것이다.

좋다. 그럼 위 예제를 Molehill 버전으로 변경한 코드를 보자. html에 embed시에 wmode는 반드시 direct로 지정되어야 한다. Flash Builder에서 테스트 하시려면 http://blog.jidolstar.com/759 에서 그 방법을 알 수 있다.

이 코드는 아래 코드가 미리 있어야 하겠다.

com.adobe.utils.AGALMiniAssembler : http://goo.gl/ITcrn 
net.hires.debug.Stats : http://goo.gl/EgZ1s
minimalcomps : https://github.com/minimalcomps


//출처 : http://wonderfl.net/c/4VE6
package 
{
	/**
	 * FP11로의 테스트.5000 파티클.빠르지는 되지만 범용성 없음
	 */
	
	import flash.display.*;
	import flash.display3D.Context3D;
	import flash.display3D.Context3DBlendFactor;
	import flash.display3D.Context3DCompareMode;
	import flash.display3D.Context3DProgramType;
	import flash.display3D.Context3DRenderMode;
	import flash.display3D.Context3DTextureFormat;
	import flash.display3D.Context3DVertexBufferFormat;
	import flash.display3D.IndexBuffer3D;
	import flash.display3D.textures.Texture;
	import flash.display3D.VertexBuffer3D;
	import flash.geom.*;
	import flash.events.*;
	import flash.text.TextField;
	import flash.utils.*;
	import flash.geom.*;
	import net.hires.debug.Stats;
	import com.bit101.components.ComboBox;
	
	[SWF(width="465", height="465", backgroundColor="0xFFFFFF")]
	public class MolehillTest extends Sprite {
		private const TEXTURE_WIDTH:int = 2048;
		private const NUM_PARTICLE:uint = 16380; // 파티클 최고수 
		private var ROT_STEPS:int = 0;
		
		private var num_limit:uint = 5000; // 렌더링 하는 수
		
		private var forceMap:BitmapData = new BitmapData( 233, 233, false, 0x000000 );
		private var randomSeed:uint = Math.floor( Math.random() * 0xFFFF );
		private var particleList:Vector.<Arrow> = new Vector.<Arrow>(NUM_PARTICLE, true);
		private var rect:Rectangle = new Rectangle( 0, 0, 465, 465 );
		private var seed:Number = Math.floor( Math.random() * 0xFFFF );
		private var offset:Array = [new Point(), new Point()];
		private var timer:Timer;
		private var world:Sprite = new Sprite();
		private var rotBmp: BitmapData;
		private var text : TextField;
		
		private var combobox : ComboBox;
		
		
		private var context : Context3D;
		private var program : ShaderProgram;
		private var iBuffer : IndexBuffer3D;
		private var vBuffer : VertexBuffer3D;
		private var uvBuffer : VertexBuffer3D;
		private var texture : Texture;
		private var ortho : Matrix3D = new Matrix3D();
		private var r_rot_steps : Vector.<Number> = Vector.<Number>([0,0,0,0]);
		
		private var vb : Vector.<Number> = new Vector.<Number>();
		private var uvb : Vector.<Number> = new Vector.<Number>();
		private var ib : Vector.<uint> = new Vector.<uint>();
		private const vunit : int = 4;
		private const uvunit : int = 2;
		
		private var uirect : Sprite = new Sprite;
		
		public function MolehillTest() {
			
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.quality = StageQuality.BEST;
			stage.frameRate = 110;
			
			addChild(world);
			
			// 포스 맵의 초기화를 행합니다
			resetFunc();
			
			// 루프 처리
			//addEventListener( Event.ENTER_FRAME, loop );
			
			//시간차이로 포스(force)맵과 색변화의 상태를 변경할 것임
			var timer:Timer = new Timer(1000)
			timer.addEventListener(TimerEvent.TIMER, resetFunc);
			timer.start();
			
			//화살표를 생성
			var dummy:Sprite = new Sprite();
			dummy.graphics.beginFill(0xFFFFFF, 1);
			dummy.graphics.lineStyle(1, 0x0, 1);
			
			dummy.graphics.moveTo(2, 4);
			dummy.graphics.lineTo(8, 4);
			dummy.graphics.lineTo(8, 0);
			dummy.graphics.lineTo(20, 7);
			dummy.graphics.lineTo(8, 14);
			dummy.graphics.lineTo(8, 10);
			dummy.graphics.lineTo(2, 10);
			dummy.graphics.lineTo(2, 4);
			
			var bmpw : int = TEXTURE_WIDTH;
			ROT_STEPS = bmpw / 16; 
			var matrix:Matrix;
			rotBmp = new BitmapData(bmpw, 16, true, 0x0);
			var i:int = ROT_STEPS;
			while (i--)
			{
				matrix = new Matrix();
				matrix.translate( -11, -7);
				matrix.rotate( ( 360 / ROT_STEPS * i )* Math.PI / 180);
				matrix.scale(0.75, 0.75); // 크기를 줄였어요.
				matrix.translate( 8+i*16, 8);
				rotBmp.draw(dummy, matrix);
			}
			
			// 파티클을 생성합니다
			for (i = 0; i < NUM_PARTICLE; i++) {
				var px:Number = Math.random() * 465;
				var py:Number = Math.random() * 465;
				particleList[i] = new Arrow(px, py);
				//world.addChild(particleList[i]);
			}
			
			// ui
			uirect.graphics.clear();
			uirect.graphics.beginFill(0x000000, 0.8);
			uirect.graphics.drawRoundRect(0, 0, 200, 100, 16, 16);
			uirect.graphics.endFill();
			
			combobox = new ComboBox(uirect, 80, 8, "5000", new Array(500, 1000, 5000, 10000, 16380));
			combobox.selectedIndex = 2;
			num_limit = 5000;
			
			addChild(uirect);
			
			// 디버그용의 스탓트를 표시하고 있습니다
			addChild(new Stats);
			
			//
			stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, createContext3D);
			stage.stage3Ds[0].requestContext3D();
			stage.stage3Ds[0].viewPort = new Rectangle(0, 0, rect.width, rect.height);
			
		}
		
		private function createContext3D(e:Event):void 
		{
			context = (e.target as Stage3D).context3D;
			context.configureBackBuffer(465, 465, 0, false);
			context.setRenderToBackBuffer();
			context.setBlendFactors(Context3DBlendFactor.SOURCE_ALPHA, Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA);
			context.enableErrorChecking = true;
			
			text = new TextField();
			text.textColor = 0xffffff;
			text.text = context.driverInfo;
			text.width = 465;
			text.y = 450;
			addChild(text);
			
			program = new ShaderProgram(context, new VertexShader(), new FragmentShader());
			ortho = MatrixUtil.ortho(rect.width, rect.height, false);        
			r_rot_steps[0] = 1/ROT_STEPS;
			
			for (var i : int = 0; i < NUM_PARTICLE; i++) {
				// 시험 삼아 정적인 다각형 정보와 매프레임 갱신하는 위치 정보를 다른 vertexBuffer로 해 보았지만, 속도적으로는 변화 없음
				vb.push( -8, -8,  0, 0);
				vb.push(  8, -8,  0 ,0);
				vb.push(  8,  8,  0 ,0);
				vb.push( -8,  8,  0, 0);
				
				uvb.push( 0,          0);
				uvb.push( 1/ROT_STEPS,0);
				uvb.push( 1/ROT_STEPS,1);
				uvb.push( 0,          1);
				
				
				ib.push( i*4+0, i*4+1, i*4+2, i*4+0, i*4+2, i*4+3 );
			}
			vBuffer = context.createVertexBuffer(vb.length / vunit, vunit);
			vBuffer.uploadFromVector(vb, 0, vb.length / vunit);
			
			uvBuffer = context.createVertexBuffer(uvb.length / uvunit, uvunit);
			uvBuffer.uploadFromVector(uvb, 0, uvb.length / uvunit);
			
			iBuffer = context.createIndexBuffer(ib.length);
			iBuffer.uploadFromVector(ib,0,ib.length);
			
			try {
				texture = context.createTexture(TEXTURE_WIDTH, 16, Context3DTextureFormat.BGRA, false);
				texture.uploadFromBitmapData(rotBmp);
				context.setTextureAt( 1, texture );
			} catch (e:Error) {
				text.text = e.message;
			}
			
			addEventListener(Event.ENTER_FRAME, loop);
		}
		
		private function loop( e:Event ):void {
			
			context.clear(0.4, 0.4, 0.5, 1); //이 정도치에 clear가 없으면 texture가 사라진다
			
			num_limit = parseInt(combobox.items[combobox.selectedIndex]);
			
			var len:uint = num_limit < particleList.length ? num_limit : particleList.length ;
			var col:Number;
			var index : int = 0;
			for (var i:uint = 0; i < len; i++) {
				var arrow:Arrow = particleList[i];
				
				var oldX:Number = arrow.x;
				var oldY:Number = arrow.y;
				
				col = forceMap.getPixel( arrow.x >> 1, arrow.y >> 1);
				arrow.ax += ( (col      & 0xff) - 0x80 ) * .0005;
				arrow.ay += ( (col >> 8 & 0xff) - 0x80 ) * .0005;
				arrow.vx += arrow.ax;
				arrow.vy += arrow.ay;
				arrow.x += arrow.vx;
				arrow.y += arrow.vy;
				
				var _posX:Number = arrow.x;
				var _posY:Number = arrow.y;
				var rot:Number = - Math.atan2((_posX - oldX), (_posY - oldY)) * 180 / Math.PI + 90;
				var angle:int = rot / 360 * ROT_STEPS | 0;
				// Math.abs의 고속화군요
				angle = (angle ^ (angle >> 31)) - (angle >> 31);
				//arrow.rot += (angle - arrow.rot) * 0.2;
				//arrow.bitmapData = rotBmp;
				
				arrow.ax *= .96;
				arrow.ay *= .96;
				arrow.vx *= .92;
				arrow.vy *= .92;
				
				// 배치 좌표를 정수화해 둡니다
				//arrow.x = arrow.x | 0;
				//arrow.y = arrow.y | 0;
				
				( _posX > 465 ) ? arrow.x = 0 :
					( _posX < 0 ) ? arrow.x = 465 : 0;
				( _posY > 465 ) ? arrow.y = 0 :
					( _posY < 0 ) ? arrow.y = 465 : 0;
				
				vb[index++] = (_posX - 465.0/2)-8;
				vb[index++] = (_posY - 465.0/2)-8;
				vb[index++] = angle >> 0;
				index++;
				
				vb[index++] = (_posX - 465.0/2)+8;
				vb[index++] = (_posY - 465.0/2)-8;
				vb[index++] = angle >> 0;
				index++;
				
				vb[index++] = (_posX - 465.0/2)+8;
				vb[index++] = (_posY - 465.0/2)+8;
				vb[index++] = angle >> 0;
				index++;
				
				vb[index++] = (_posX - 465.0/2)-8;
				vb[index++] = (_posY - 465.0/2)+8;
				vb[index++] = angle >> 0;
				index++;
			}
			vBuffer.uploadFromVector(vb, 0, num_limit*4); 
			
			context.setProgram(program.program);
			
			context.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, ortho, true);
			context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 4, r_rot_steps);
			
			context.setVertexBufferAt(0, vBuffer, 0, Context3DVertexBufferFormat.FLOAT_2);
			context.setVertexBufferAt(1, vBuffer, 2, Context3DVertexBufferFormat.FLOAT_2);
			context.setVertexBufferAt(2, uvBuffer, 0, Context3DVertexBufferFormat.FLOAT_2);
			
			context.drawTriangles(iBuffer, 0, 2*num_limit);
			context.present();
		}
		
		private function resetFunc(e:Event = null):void{
			forceMap.perlinNoise(117, 117, 3, seed, false, true, 6, false, offset);
			
			offset[0].x += 1.5;
			offset[1].y += 1;
			seed = Math.floor( Math.random() * 0xFFFFFF );
		}
	}
}

import com.adobe.utils.AGALMiniAssembler;
import flash.display3D.Context3D;
import flash.display3D.Context3DProgramType;
import flash.display3D.Program3D;
import flash.geom.Matrix3D;

import flash.display.*;

class Arrow// extends Bitmap
{
	public var rot:int = 0;
	public var vx:Number = 0;
	public var vy:Number = 0;
	public var ax:Number = 0;
	public var ay:Number = 0;
	public var x:Number = 0;
	public var y:Number = 0;
	
	function Arrow( x:Number, y:Number) {
		this.x = x;
		this.y = y;
	}
}

class ShaderProgram
{
	public var program : Program3D = null;
	
	public function ShaderProgram(context : Context3D, vsh : AGALMiniAssembler, fsh : AGALMiniAssembler)
	{
		program = context.createProgram();
		program.upload(vsh.agalcode, fsh.agalcode);
	}
}

class VertexShader  extends AGALMiniAssembler
{
	//
	// 
	//  vBuffer0(x,y)     --> attribute(0) : va0.xy
	//  vBuffer1(rot,rsv) --> attribute(1) : va1.x, va1.y
	//  uvBuffer2(u,v)    --> attribute(2) : va2.xy
	//
	// 
	//  projmatrix(transposed)   --> | vc0.x  vc1.x  vc2.x  vc3.x |
	//                               | vc0.y  vc1.y  vc2.y  vc3.y |
	//                               | vc0.z  vc1.z  vc2.z  vc3.z |
	//                               | vc0.w  vc1.w  vc2.w  vc3.w |
	//
	//  texture coord step       --> vc4.x   (1/rotation steps)
	//
	// 
	//  position                 --> op.xyzw
	//
	// 
	//  texture coord            --> v0.uv
	//
	//  m44 op, va0, vc0             position = (x,y) * projmatrix
	//  mov v0, va2                  uv = (u,v)
	//  mul vt0.x, va1.x, vc4.x      
	//  add v0.x, va2.x, vt0.x       u = u + (rot*texture_coord_step)
	//
	private var src : String =
		"m44 op, va0, vc0 \n" + // m33라도 좋을지도
		"mov v0, va2 \n" +
		"mul vt0.x, va1.x, vc4.x \n" +
		"add v0.x, va2.x, vt0.x \n" +
		"";    
	
	public function VertexShader()
	{
		assemble(Context3DProgramType.VERTEX, src);
	}
}

class FragmentShader  extends AGALMiniAssembler
{
	private var src : String =
		"mov ft0, v0\n" +
		"tex ft1, ft0.xy, fs1 <2d,repeat,nearest>\n" + 
		"mov oc, ft1\n";
	
	public function FragmentShader()
	{
		assemble(Context3DProgramType.FRAGMENT, src);
	}
}
class MatrixUtil
{
	public static function ortho(w : int, h : int, rev : Boolean) : Matrix3D
	{
		return new Matrix3D(Vector.<Number>([2/w, 0, 0, 0,  0, rev?-2/h:2/h, 0, 0,  0, 0, 1, 0,  0, 0, 0, 1]));
	}
	
}


위 코드는 아래 링크를 통해 실행해 볼 수 있다.
http://escargot.la.coocan.jp/molehill_fast/index.html


아래 화면은 화살표를 5000개로 맞춰놓고 렌더링한 캡쳐화면이다. 기존방식보다 4000개 이상 처리함에도 불구하고CPU 점유율과는 비교가 안될 정도로 떨어졌고 FPS도 50~60사이이다. 


아래 화면은 16380개로 올린 결과 화면이다. CPU 점유율은 24%로 전보다 다소 올라갔으나 FPS는 변함이 거의 없다. CPU 점유율이 올라간것은 as3 코드 loop 부분이 더 복잡해서일 뿐이다. 


두가지 결과만 보더라도 Molehill을 통한 GPU 가속은 꽤나 유용함을 금방 알 수 있다. 
 
아래 링크의 예제를 통해 같은 방법으로 비교해보는 것도 괜찮을 것 같다.

기존방법 Clear Water: http://wonderfl.net/c/9R8a
Molehill이용 Clear Water  : http://wonderfl.net/c/enj6
관련 기술 설명 : http://goo.gl/eySSG


3. Molehill 기반 2D framework 소개 

Molehill을 이용해 2D를 렌더링하는 것이 얼마나 이득이 될 수 있는가 알아보았다. 이러한 이득은 곧바로 필요성으로 이어져 관련 framework 개발로 이어질 수 있다. 찾아보니 실제로 그런 프로젝트가 있었다.

소개하고자 하는 M2D라는 오픈소스 프로젝트는 Molehill을 이용해 2D을 표현하도록 하도록 하는데 목적이 있다. Molehill의 저수준의 언어 접근성을 고수준으로 만들어주어 개발이 용이하게 만든것이 특징이다. 이는 iOS기반 GPU가속 2D라이브러리인 Cocos2D와 같은 필요성에서 탄생된 프로젝트이다.



아직 진행중인 프로젝트이기 때문에 관련된 문서도 없고 기능도 제약이 있다. 

ND2D라는 라이브러리도 있다. 


ND3D를 만든 개발자가 Molehill 버전의 2D 엔진인 ND2D를 만든것이다.
위 사진은 아래 링크에서 실행해 볼 수 있다.
http://www.nulldesign.de/2011/03/10/nd2d-molehill-2d-engine/ 

소스는 아래 링크에서 다운받을 수 있겠다.
https://github.com/nulldesign/nd2d

M2D보다는 ND2D가 더 좋아보인다. ^^


4. 정리하며 

지금까지 내용을 통해 Flash Player 차세대 렌더링 엔진 Molehill을 이용해 2D 표현시 어떤 장점이 있을까 객관적인 자료를 찾아보고 분석해보았다. Molehill을 이용해 GPU 가속을 할 수 있다는 것 자체는 큰 장점이긴 하나 그만큼 연구해야할 내용은 더 많아진 것은 사실이다. 앞으로 Flash Player가 모바일에서 활약하는 날이 올때 그 빛은 엄청 발할 것이라 판단한다. 그러므로 우리 개발자들은 미리 준비할 필요가 있겠다.

Flash Player 11 Incubator를 통해 차세대 3D 엔진인 Molehill을 미리 경험할 수 있다는 것은 큰 혜택이다. 새롭게 연구해야하는 과정은 참 어렵다. 필자도 이런 어려운 과정을 겪고 있으며 혼자 이런 연구를 하는게 무척이나 힘들다. 뭔가 이러한 일을 함께 하는 사람들이 많았으면 하는 바램이다.


5. 관련글


 글쓴이 : 지돌스타(http://blog.jidolstar.com/763)
저작자 표시 비영리 동일 조건 변경 허락
신고

Adobe Flash 3D , , , , , , , , , , , , , , , , , , , ,

  1. 멋져요!! 몰힐 정식 배포할 때까지 기다리려고 했는데 당장 배우고 싶어집니다.

  2. 코드도 간단하게 훑어봤는데
    angle = (angle ^ (angle >> 31)) - (angle >> 31)
    arrow.x = arrow.x | 0
    이런 비트 연산자 이용하는 거 신기하네요 ㅇ_ㅇ

  3. ㅎㅎ 그러게요~
    Molehill 너무 어려워요. Shader도 잘모르는데... 이런게 나오니... 어찌 할지 막막하다는... 글은 적긴 했는데 분석은 아직 못해서 계속 뚫어지게 쳐다보고 잇습니다. 이런 관련된 글들이 한국에도 많이 나와줬으면 하는 바램이네요 ㅎㅎ

  4. 저번에 픽셀 벤더 문서를 번역했을 때, 픽셀 벤더 활용 사례가 별로 없는 것 같아 실망했는데, 몰힐이 나오니 픽셀 벤더 3D는 흥하겠죠? 픽셀 벤더 3D를 파볼까 생각 중입니다~

  5. 이번에는 분명 픽셀밴더의 힘이 중요해질겁니다. 이전에도 중요했지만 이제 더 중요해질거라는... 이 분야에 대한 학습이 절실하게 필요해요 ㅎㅎ

  6. 형님 너무 잘 봤습니다. 정리를 너무 잘 해주셨네요 ㅎㅎ
    공부해야 할 게 참 많네요~

  7. 그러게~ 나도 공부해야할것이 많아 ㅋ

  8. 이거 완전 기대되네요.
    iOS에서까지 성능이 보여지면 진짜 좋겠네요.

  9. 언리얼 엔진이 actionscript로 이식되는 그 날을 위하여~ ㅋㅋㅋㅋㅋ

  10. 정작 actionscript는 호스트코드일 뿐이고 실질적인 계산 및 렌더링은 전혀 담당하지 않는다는... ㅎㅎ

  11. 걍 어웨이4에서 오쏘고날 렌즈로 해도 비슷해요.
    추가로 이벤트 처리나 기타 처리도 다 되고...

  12. 그러게요. 실험해 봐야 겠습니다. 흥미롭네요.

  13. http://www.bytearray.org/?p=2555
    여기에 Diney Bomfim이라는 사람이 도발적인 댓글을 올렸네요 '-'

    [좋아요. 이제야 몰힐에 흥미가 동합니다. 당신 OpenGL programmable pipeline을 생각했군요. 저도 애플 개발자고 OpenGL을 씁니다. 그리고, 의문이 한 가지 있습니다..

    빌어먹을, 왜 어도비는 새로운 섀이더 언어(AGAL)을 만드려는 겁니까? OepnGL SL을 쓰지 않고? 오 물론 PC의 DirectX를 위해선 뭔가 변환이 필요하겠지만 SL은 이런 AGAL보단 훨씬 읽기 쉽습니다!! 맙소사... op, vc, ft, ff, fa, si sa su, mi mo mu... 이게 뭔가요?

    제 말이 옳다는 걸 아시죠! "gl_Position" "gl_FragColor" "gol_FrontFacing" "uniform" "varying"이 훨씬, 훨씬 좋습니다!! 그리고 제발 픽셀 벤더는 말하지 마요!! 이건 애들 용입니다!! 진짜 개발자는 섀이더와 SL을 애호합니다!! 정말로, 몰힐에 더 가독성 좋은 섀이더 언어를 도입하기를 바랍니다.]

  14. 어휴~ 저도 저것때문에 머리가 아픕니다. 결국 Away3D나 Alternative3D를 잘활용하는것이 길일듯~ 결국 이 넘들은 그런 부분을 다 해주었으니 말이죠. 그래도 욕심인게... 좀 일단 원리는 알아둬야겠다는... ^^

  15. 저 사람 말처럼 하면.... flash player가 무거워질 것같아요. AGAL을 쓰는 것은 결국 flash player를 가볍게 만드려는 의도도 포함된 것 아닐까요?

  16. Blog Icon
    파란화면

    저도 글에 공감합니다.

    OpenGL SL이면 기존의 자료도 많고 nVidia Cg 같은 고수준 셰이더 언어를 컴파일해서 만드는 것도 가능하니 훨씬 쉽게 셰이더를 만들수도 있을겁니다.
    요즘은 어셈으로 셰이더를 만드는 경우가 거의 없는데, 어도비는 어쩔셈인지 모르겠네요.

    픽셀벤더는 좀 어정쩡했죠.

    AGAL을 쓴다고 해서 무거워진다고 생각하진 않습니다. AGAL은 한번 더 DirectX나 OpenGL용으로 변환되어져야 하죠. OpenGL SL이었으면 적어도 OpenGL에서는 바로 쓸 수 있으니 훨씬 좋죠.

  17. 잘 봤습니다. 2D도 날라다닐 수 있겠군요~
    근데 몰힐은 언제쯤 정식으로 나올까요?

  18. 글쎄요. 제가 adobe는 아니니~ ㅎㅎ 그냥 예상키로는 올해 4/4분기나 내년 초에는 정식 배포되지 않을까요?

  19. Blog Icon
    winnie

    아 정말 잘봤습니다. lab 사이트에서 뒤적뒤적 구글 뒤적뒤적이다가 역시 지돌님 하면서 읽었네요..
    지돌스타님 블로그에 처음 댓글을 쓰게만들기도 했네요.. url 퍼갑니다. ^^/

  20. 오랜만이예요. ㅎㅎ
    제 글은 수박겉핧기 정도라서요.
    ㅎㅎ

  21. 내용 참 좋네요~~ 어찌 이런 복받을 포스팅을 하셨을까~ ㅎㅎㅎ 잘 보고있습니다. |o|

  22. ㅎㅎ 버터백통님도 복받을 포스팅 많이 하시잖아요. 제 글을 보셨으니 이제 이 부분에 한배를 타신겁니다.

  23. Blog Icon
    한석중

    세미나 잘 보고 들었습니다.^^
    글로 보는것 보다는 말로 한번 설명듣는게 역시나 훨씬 더 받아들이기 좋네요..
    고생하셨어요^^ 쵝오였습니다.

  24. ㅎㅎ 다행이네요. 자료는 방대하고 시간은 짧아서 제대로 전달을 해드리지 못해 송구합니다. ^^

  25. Blog Icon
    모모

    글 잘 읽고 갑니다 ^^~ 잘 정리해주셔서 감사합니다~

  26. Blog Icon
     
  27. 개발환경이 먼저 만들어져야 합니다.

    Adobe Flash Player 11 Incubator의 "Molehill" 3D를 활용하기 위한 개발환경 만들기 : http://blog.jidolstar.com/759

    를 보세요.