HTML5의 Canvas이미지를 가져다가 안드로이드에서 Bitmap으로 사용할 수 있다. (어떤 경우에 있겠지?) 아래 코드는 그걸 실험해 보려고 만든 테스트 코드이다. 일단 HTML5의 Canvas를 사용해 그림을 그린후 canvas.toDataURL().toString()을 사용해 Base64 문자열을 뽑아낸다. 


<!DOCTYPE HTML>
<html>
  <head>
    <style>
      body {
        margin: 0px;
        padding: 0px;
      }
      #myCanvas {
        border: 1px solid #9C9898;
      }
    </style>
    <script>
      window.onload = function() {
        var canvas = document.getElementById("myCanvas");
        var context = canvas.getContext("2d");

        // begin custom shape
        context.beginPath();
        context.moveTo(170, 80);
        context.bezierCurveTo(130, 100, 130, 150, 230, 150);
        context.bezierCurveTo(250, 180, 320, 180, 340, 150);
        context.bezierCurveTo(420, 150, 420, 120, 390, 100);
        context.bezierCurveTo(430, 40, 370, 30, 340, 50);
        context.bezierCurveTo(320, 5, 250, 20, 250, 50);
        context.bezierCurveTo(200, 5, 150, 20, 170, 80);

        // complete custom shape
        context.closePath();
        context.lineWidth = 5;
        context.strokeStyle = "blue";
        context.stroke();
		
	document.getElementById("canvasData").innerHTML = canvas.toDataURL().toString();
		
      };

    </script>
  </head>
  <body>
    <canvas id="myCanvas" width="578" height="200"></canvas>
    <div id="canvasData"></div>
  </body>
</html>


결과화면은 다음과 같다. 



base64 문자열을 복사해서 다음 안드로이드 코드에서 사용해본다.
package com.cookilab.base64bitmap;

import java.io.ByteArrayOutputStream;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.Base64;
import android.view.View;

public class TestActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView( new TestView(this) );
    }
    protected class TestView extends View {
        public TestView(Context context) {
            super(context);
        }
        public String encodeTobase64(Bitmap image) {
            Bitmap immagex=image;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();  
            immagex.compress(Bitmap.CompressFormat.JPEG, 100, baos);
            byte[] b = baos.toByteArray();
            String imageEncoded = Base64.encodeToString(b,Base64.DEFAULT);
            return imageEncoded;
        }
        public Bitmap decodeBase64(String input) {
            byte[] decodedByte = Base64.decode(input, 0);
            return BitmapFactory.decodeByteArray(decodedByte, 0, decodedByte.length); 
        }    
        public void onDraw(Canvas canvas){
            String base64 = "iVBORw0KGg............==";
            Bitmap bitmap = decodeBase64(base64);
            canvas.drawBitmap(bitmap, null, new Rect(0, 0, 300, 100), null);
        }
    }   
}




그래서 뭘 하고 싶은거냐?

자바스크립트와 안드로이드간 통신이 가능하므로 이미지도 전송할 수 있다는 것을 보여준 셈이다. 


끝 ^^


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

이전에 "10만개 입자를 이용한 유체 시뮬레이션 실험"이라는 제목으로 글을 적었다. 이 글로 10만개의 1x1 픽셀의 입자를 되도록 빠르게 렌더링하기 위한 기술을 습득할 수 있었다.

이번에는 조금 더 실용적으로 접근한다. 이 실험은 입자 유체 시뮬레이션의 연장전으로 입자대신 화살표를 이용한다. 소개하고자 하는 것은 원래 일본의 Yasu 아이디를 가진 개발자가  Wonderfl에 단순한 학습자료 용도로 올린 소스였다. 이 소스는 계속 Fork(원본소스를 퍼가서 수정하는 작업)가 거듭됨에 따라 매우 흥미롭고 재미있는 실험으로 탈바꿈하기 시작했다. 수십번의 Fork와 수백번의 favorite로 지정된 이 실험은 필자로 하여금 Flash 속도 개선에 대한 아이디어를 얻을 수 있다는 기대를 가지게 하기에 충분했다.


최종 실험 결과 화면



이 글은 Yasu님의 블로그에 소개된 글을 다시 한번 검토해보고 분석하는 정도이다.
(참고로 Yasu님은 Wonderfl과 블로그에서 clockmaker 아이디로 활동중이며 꽤 재미있는 컨텐츠를 많이 생산하고 있다.)

앞으로 소개할 내용은 아래 첨부파일내 ActionScript 3.0 코드를  참고하길 바란다.  Flash Builder 또는 Flash CS4에서 Flash Player 10기반으로  테스트하면 되겠다. 





BitmapData 배열을 활용한 속도 증가

Yasu님은 그의 블로그를 통해 Flash 속도 개선을 위한 매우 좋은 아이디어를 알려주었다.

[다음 Flash의 속도를 체험] BitmapData를 배열에 저장하면 2 ~ 3 배 속도 - 일본어 번역기를 통해 보세요.


이 실험은 2가지 경우의 예를 들고 있다. (첨부파일의 bitmap_arrow_01.as, bitmap_arrow_02.as를 참고)

  1. 화살표를 정해진 수만큼 Sprite로 생성하고 회전/이동 처리 
  2. 화살표의 모양을 회전값에 따라 BitmapData로 만들어 배열에 저장한 뒤, 정해진 수만큼 화살표를 렌더링할 Bitmap을 만든다. 1번과 같이 Bitmap의 위치를 지정하되 화살표의 방향은 이미 만든 BitmapData를 이용하여 회전된 모양 처리

(Wonderfl이 아닌 첨부파일 소스에서)
1번 예제는 화살표 500개로 FPS(frame/seconds)가 50정도의 속도가 나온다. 2번예제는 화살표 3000개로 FPS 50정도 나온다. 같은 FPS이지만 화살표를 더 렌더링할 수 있는 2번 예제의 경우가 훨씬 빠르다는 것을 알 수 있다. (물론 FPS는 개인 PC사양에 따라 다르게 나온다.)

일반적으로 많은 Flash 입문자가 처음 접하는 예제는 1번 일것이다. 하지만 화살표 모양의 Vector 데이터가 500개나 되기 때문에 회전, 이동때 마다 많은 수의 Vector정보를 렌더링하기 위해 계산이 많아질 수 밖에 없다. 2번 예제의 경우에는 이 문제를 해결하기 위해 화살표 회전값에 따라 미리 BitmapData를 만들어 놓고 실제 렌더링시에는 회전각도에 따라 선별적으로 미리 만든 해당 화살표 BitmapData를 가져와 사용할 수 있도록 만들었다. 일단 이렇게 하면 렌더링을 위한 계산량이 급격하게 떨어지므로 FPS 증가를 도모할 수 있게 된다.

이것만 보더라도 1번 예제처럼 만들고 Flash는 너무 느려요 라고 말하시는 분들이 있다면 반성해야한다. 어떤 언어로 만들던지 1번처럼 만들면 당연히 느려진다. 이것은 필요하다면 속도 개선을 위한 노력이 필요하다는 것을 말해주는 단적인 아주 좋은 예제이다. ^^


Fork! Fork!

Wonderfl 사이트의 강점 중에 하나가 바로 Fork 기능이다. 이를 이용해 다른 사람이 만들어 놓은 ActionScript 코드를 더욱 개선하거나 발전시킨 코드로 만들 수 있고 공유할 수 있다. 실제로 이 기능을 통해 Yasu님이 올려놓은 코드는 바로 다른 개발자들의 입맛에 맛게 다음과 같은 암묵적인 약속에 의해 변경되기 시작했다. 

  1. 속도에 따라 화살표 색이 달라진다.(빠를수록 빨강, 느릴수록 파랑)
  2. 속도가 빠른 화살표가 가장 화면 상단에 배치된다. 
  3. 위 조건으로 했을 때 최대 속도를 내도록 코드를 갱신한다.

즉, 위 조건은 보는 이로 하여금 더욱 예쁘고 멋진 효과를 보여주도록 하는 구체적인 목표인 것이다. 

이 시발점은 Wonderfl 아이디 keno42 님으로부터 시작한다.

속도개선 전, 색깔을 입힌 화살표 모습


- 화살표의 속도에 따라 색과 레이어를 달리 지정하도록 수정 (첨부파일의 bitmap_arrow_04.as 참고)

확실히 밋밋했던 화살표가 색이 들어가니 보기가 좋아졌다. 하지만 이에 따라 FPS가 급격하게 떨어지게 되었다. 가장 큰 이유는 2번 조건때문에 화살표의 레이어 위치를 수시로 변동시켜 주는 아래와 같은 코드가 추가되었기 때문이다.

arrow.parent.removeChild(arrow);
worldAlphaChildren[speed].addChild(arrow);	

이 때문에 FPS가 급격히 줄어들어 앞선 실험에서 사용된 화살표 3000개를 1000개로 줄일 수 밖에 없었다. 이렇게 해서야 FPS 57정도 나왔다. 


(참고)Fork된 코드는 diff기능으로 비교하자.


Wonderfl의 강력한 기능중에 하나는 Fork된 원본코드와 Fork를 통해 수정된 코드를 비교할 수 있다는 것이다. 위 Wonderl의 캡쳐 화면에서 볼 수 있듯이 Fork한 코드는 forked from과 원래 제작자 및 코드의 제목이 나와 있고 그 옆에 diff(104)가 표시되어 있다. 이 말은 이 소스가 원본 소스와 104개 줄이 추가/삭제/수정 되었다는 것을 의미한다. 이것을 클릭하면 다음과 같은 화면이 나온다. 


이것을 사용하면 어느부분이 수정되었는지 바로 알 수 있기 때문에 매우 유용하다.


속도개선 1 (bitmap_arrow_05.as 참고)

첫번째로 생각한 것은 마우스 이벤트 전파부분이였다. 빠른 속도를 가진 화살표가 위로 올라오게 하는 조건을 만들기 위해 앞선 코드에서는 단계별 레이어를 만들었다. 이 레이어는  Sprite로 만들어졌다. Sprite는 DisplayObjectContainer를 확장한 클래스로 자식을 가질 수 있는 시각객체를 렌더링하는데 기본이 되는 클래스중 하나이다. 시각객체들은 자식과 부모관계(addChild()에 의해)를 가지게 되면 이벤트 전파 메커니즘에 따라 이벤트가 전파된다. 그중에 마우스 이벤트가 대표적이다. 하지만 이 이벤트 메커니즘은 Flash 속도 저하에 큰 요소가 되기도 한다. 그러므로 이벤트 전파가 필요 없는 곳에는 사용하지 않도록 강제로 설정해줄 필요가 있다. 그 역할을 하는 것은 Sprite속성중에 mouseChildren, mouseEnabled 이다. 화살표가 올라갈 부모로서의 Sprite 레이어들은 어떠한 마우스 이벤트를 받을 필요가 없기 때문에 이들 속성을 모두 false로 지정하도록 한다. 이 설정으로 약간의 FPS개선이 있었다. 

두번째로 arrow.parent.removeChild(arrow); 부분을 삭제하는 것이였다. 시각객체는 2개 이상의 부모가 존재할 수 없다. 그러므로 otherParent.addChild(arrow); 하는 것만으로 arrow.parent.removeChild(arrow)가 이미 실행된 것이다. 이는 비싼 실행 비용을 지불하므로 제거하면 확실히 속도 개선이 된다. 실제로 FPS가 70이상으로 개선되며 거의 20이상 증가하게 되었다. 


속도개선 2 (bitmap_arrow_06.as 참고)
아직도 비싼 실행 비용을 지불하는 것이 있다. 바로 addChild()이다. 속도에 따라 레이어를 변경하더라도 굳이 변경할 필요가 없는 경우도 있다. 이러한 경우에는 선별적으로 변경되도록 해야한다. 아래 코드처럼 단순한 조건문 하나 넣어 쓸데없이 addChild를 실행하지 않게 하는 것만으로 속도 개선이 된다. 

if (arrow.parent != world.getChildAt(speed)) {
	Sprite(world.getChildAt(speed)).addChild(arrow);
}
이 작업으로 무려 FPS가 90 이상으로 상승했다. 속도개선 1 보다 약 20정도가 증가했다. 


속도개선 3 (bitmap_arrow_07.as 참고) 
속도개선 2에서 getChildAt()하는 것은 그리 빠른 방법이 아니다. 그래서 Array를 이용해 아래와 같이 바꾸게 된다.
if( arrow.parent != childrenArr[speed] )
	childrenArr[speed].addChild(arrow);	
원래 실험자는 개선이 있었다고 하나 필자는 그리 큰 개선은 못보았다.

속도개선 4 (bitmap_arrow_08.as 참고)
화살표는 정사각형 영역이 아닌 직사각형 영역에 그려진다. 무슨말인가? 회전된 화살표가 그려지는 영역은 항상 작아졌다가 커졌다가 한다. 이는 화살표의 영역을 담은 BitmapData가 실제 화살표 크기보다 클 수 있다는 것을 의미한다. 실제 화살표 크기보다 큰 BitmapData에서 쓸데 없는 부분을 제거하는 작업을 트리밍(trimming)이라고 한다. 이 작업으로 실제로 렌더링하는 화살표의 적당한 크기의 영역만을 가진 BitmapData를 만들기 때문에 물리적인 렌더링 시간 향상에 도움을 줄 수 있다.

while (i--) { //각도에 따라 화살표 비트맵 생성 
	matrix=new Matrix();
	matrix.translate(-11, -11);
	matrix.rotate((360 / ROT_STEPS * i) * Math.PI / 180);
	matrix.translate(11, 11);
	
	temp = new BitmapData(22,22,true,0x0);
	temp.draw(dummyHolder, matrix);

	//트리밍 처리(중심점의 조정은 하지 않는다)
	rect = temp.getColorBoundsRect(0xff000000, 0x00000000); //알파채널이 0이 아닌 사각형 이미지 경계 
	rotArr[i + k]=new BitmapData(rect.width, rect.height, true, 0x0);
	rotArr[i + k].copyPixels(temp, rect, new Point(0,0));
}

위 코드에서 보는데로 BitmapData의 getColorBoundsRect() 메소드를 이용해 트리밍 처리를 하고 있다. 이 작업으로 FPS를 거의 100까지 올릴 수 있었다.


속도개선 5 (bitmap_arrow_09.as 참고)
지금까지의 방식은 화살표 하나를 표현하는 도구로 Bitmap을 사용했다. 이것은 시각객체(DisplayObject)의 하나이다. 이 방식은 화살표 1000개를 그릴려면 시각객체 1000개가 필요하다는 말과 같다. 속도개선 1~4까지 하면서 사실 거의 최대한으로 속도개선했다. 이제 속도개선에 기대할 수 있는 마지막 단계는 바로 화살표를 렌더링하는 방법 조차도 BitmapData 를 이용하는 것이다. 이때는 1000개의 시각객체가 아니라 1000개의 위치/방향 정보를 담는 객체가 필요하므로 일단 상대적인 메모리 부담이 줄어든다. 그리고 기존 방식과 거의 동일하나 그려지는 Canvas가 이제는 Sprite를 이용한 레이어가 아니라 BitmapData 하나가 된다. 그러므로 화살표는 BitmapData의 copyPixels()메소드를 이용해 그리게 된다. 또한 화살표의 속도에 따른 위치를 정렬하기 위해서 Array.sortOn()함수를 이용한다. 

결과적으로 FPS가 120까지 상승했으며 초반에 60미만이였던 것에 비해서 거의 2배이상 빨라졌다. 그뿐인가? ColorTransform적용으로 화살표 궤적도 보여줄 수 있게 되었다. 

속도개선 최종화면



실행 : http://wonderfl.net/code/834201fba6c3562ca6fd7a0f1f229138b263b446 


조금더 예쁘게!!!!
지금까지 속도개선은 모두 Wonderfl의 Fork기능을 통해 각각 다른 사람들에 의해 구현된 것들이다. 최초에 화살표를 이용한 예제를 만든 Yasu님은 자신의 블로그에 지금까지 필자가 다루웠던 내용을 요약하면서 마지막으로 한번더 Fork를해 더 예쁘게 보여주기 위한 코드로 수정했다. 

마지막으로 더 멋진 화면을 만들기 위한 작업 결과




분명히 이전 코드와 속도상에 차이는 없으면서 더욱 아름다운(?) 결과물을 도출해냈다. 

그리고 1000개의 화살표가 아닌 3000개로 바꾸어도 FPS가 40이상 나왔다. 


정리하며
1픽셀짜리 데이터를 다룰때는 10만개나 다루었다가 그와는 상대적으로 너무 적은 3000개 화살표 렌더링하는 것으로 전환되면서 너무 숫자가 급감하는 것처럼 보일 수 있다. 하지만 실제로 렌더링하는 데이터 요소는 10만픽셀 이상으로 많은 데이터를 다루는 것이다.(조금만 생각하면 감이 올거다.)  렌더링 자원 숫자를 줄이는 것도 중요하지만, 그 자원이 어떤 조건의 것인가와 어떻게 렌더링 할 것인가도 역시 중요하다. 결국, 화면 렌더링을 어떻게 하느냐에 따라 Flash 애플리케이션의 속도와 밀접한 관계가 있는 것이다. 실무에서 제약된 환경의 디바이스나 서비스의 경우라면 이러한 노력은 필수이다. 

이 글에 소개된 아이디어는 정답은 아니다. 더 분석하고 더 최적화할 여지가 있다. 

Wonderfl은 아주 고급의 스킬을 다루지는 않지만 적어도 중고급 실력에 도달하기 위한 예제를 찾는데는 이만한 곳이 없다고 생각한다. 필자는 이곳에 올라온 코드들을 보면서 수없이 감격하고 있다. 

마지막으로 라이브 코딩을  SNS 영역으로까지 승격한 Wonderfl을 이용해 다양한 실험들을 하는 일본의 개발 문화를 보면서 항상 감탄한다. 아무쪼록 이러한 일본의 개발 문화가 한국에도 정착되길 희망한다. 


글쓴이 : 지돌스타(http://blog.jidolstar.com/671)
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
 

Flash에서 이미지를 표현하기 위한 방법은 VectorBitmap이 있다.

 

Vector은 점,선,면등의 단순한 수학적 정보만 이용하여 이미지를 표현한다. 그렇게 때문에 적은 용량으로 이미지를 표현할 수 있으며 특히 확대/축소에도 깨지지 않는 이미지를 표현하는데 탁월하다. 또한 곡선 및 각종 이펙트 표현력도 훌륭하다. 적은 용량으로 이미지를 표현할 수 있기 때문에 메모리 관리에 도움이 된다. 하지만 복잡한 이미지의 경우 도리어 퍼포먼스의 저하가 일어날 수 있으며 이 경우 이동과 같은 에니메이션을 줄 경우 CPU연산이 많아지는 단점이 있다.

 

Bitmap은 이미지를 표현하기 위해 Pixel정보를 이용한다. 하나의 Pixel정보에는 ARGB값을 가진다. Pixel 하나하나 정보를 가지고 다루므로 복잡한 이미지를 나타내는데 좋으며 Vector와 달리 이동 애니메이션을 주더라도 CPU연산이 적다. 하지만 이미지가 클수록 Pixel 정보량이 제곱단위로 커지기 때문에 확대/축소시에 메모리가 과다하게 사용될 여지가 있다. 잘못사용하면 너무 큰 이미지로 인해 Flash Player가 다운될 수도 있다.

 

아래는 필자가 다루고자 하는 Vector와 Bitmap 이미지의 장단점을 언급한다.(아래 장단점은 완벽한 것은 아니다. 가령 Vector라고 해도 객체수가 적으면 속도에 무리가 없기 때문이다. 적어도 필자가 하고 있는 프로젝트 내에서 이런 문제가 있다는 것을 미리 언급한다.)

 

  • Vector 이미지
    - 메모리 효율이 좋다.(확대/축소시에도 메모리 점유율 변화 없음)
    - 이동 애니메이션 처리가 느리다.(CPU 연산이 커짐)
  • Bitmap 이미지
    - 메모리 효율이 나쁘다.(확대시 메모리 점유율 커짐)
    - 이동 애니메이션 처리가 빠르다.(CPU 연산 적음)

 

Vector 이미지와 Bitmap 이미지를 연구하게된 배경은 필자가 스타플 서비스(http://starpl.com/)의 별지도를 만드는데 이 부분에 대한 정보가 필요했기 때문이다. 먼저 아래 글을 읽어보면 이해가 쉽겠다.

 

[Flash,ActionScript 3.0]DisplayObject cacheAsBitmap 속성의 적용시점 문제

[Flex/AIR] 메모리의 무서운 적! DisplayObject의 cacheAsBitmap 속성

 

스타플의 별지도

 

구글맵과 같은 일반 지도의 경우 타일(Tile)들의 구성요소는 Bitmap 이미지이다. 최적화된 Bitmap이미지이기 때문에 렌더링 시간도 짧고 이동도 빠르다. 필자가 작업한 별지도는 천체 데이터 및 사용자 별 데이터를 읽어와 Vector 이미지로 렌더링하는 방식이다. 따로 데이터 가공 및 렌더링 시간이 필요하고 Vector이미지는 일단 이동 애니메이션이 느리다. 별이 반짝거리므로 단순히 Bitmap으로 바꿔서 처리할 수 있는 것도 아니다. 결국 스타플 별지도는 기본적으로 Vector 이미지로 타일을 구성해야한다.

 

앞서 설명했듯이 Vector 이미지를 이용하면 메모리 문제는 발생하지 않는다. 문제는 이동시 속도가 느리다는 것이다. 마우스로 지도를 드래그해서 이동하는 경우 저사양 컴퓨터에서는 버벅거림이 발생한다. Vector 이미지 타일로 구성된 지도의 단점이다. 그래서 이것을 극복하기 위해 첫번째로 사용하려했던 기능이 flash.display.DisplayObject의 cacheAsBitmap 속성이였다. 이 속성을 true로 설정하면 타일이 Bitmap으로 캐싱되고 실제로 적용후 이동처리해보면 훨씬 부드러운 이동 애니메이션을 볼 수 있다.

 

하지만 cacheAsBitmap은 Bitmap 이미지를 만드는 것이므로 확대/축소에서 문제가 있다. 이 속성을 true로 설정하고 확대하면 메모리가 기하급수적으로 늘어난다. 그래서 간단하게 확대/축소전에 cacheAsBitmap을 false로 지정하면 되겠다고 생각했다. 하지만 이 속성이 false로 설정된 순간 캐싱된 Bitmap을 해제시켜주지 못했다. 결국 false로 지정하고 확대해도 여전히 메모리가 증가한다.

 

이를 극복하기 위해 BitmapData를 이용했다. 만들어진 타일(Tile) 클래스에 bitmapMode라는 속성을 정의하고 true이면 Bitmap이미지를 false이면 Vector이미지를 보일 수 있도록 만든다. 그렇게 해서 이동이 발생시에는 Bitmap이미지를 사용하고 평상시 또는 확대/축소시에는 Vector 이미지를 사용할 수 있도록 한다. 아래는 이것을 구현하는 간단한 코드이다.(완벽한 코드가 아니다. 단지 예시일 뿐이다.)

 

this.addEventListener( Event.REMOVED_FROM_STAGE, onRemovedFromStage, false, 0, true );

public function bitmapMode( value:Boolean ):void
{
	if( _bitmapMode == value ) return;
	_bitmapMode = value;
	
	.....

	if( _bitmapMode )
	{
		var bitmapData:BitmapData = new BitmapData( getWidth()+100, getHeight()+100, true, 0x00000000 );
		var matrix:Matrix = new Matrix();
		matrix.translate( 50, 50 );						
		bitmapData.draw( this, matrix, null, null, null, false );
		bitmapImage = new Bitmap( bitmapData );
		bitmapImage.x = -50;
		bitmapImage.y = -50;
		addChild( bitmapImage );
		removeChild( vectorImage );
	}
	else
	{
		addChild( vectorImage );
		removeChild( bitmapImage );
	}

	.....
}


public function disposeBitmap():void
{
	bitmapMode = false;
	if( bitmapImage )
	{
		bitmapImage.bitmapdata.dispose();
		bitmapImage = null;
	}
}

private function onRemovedFromStage( event:Event ):void
{
	this.removeEventListener( Event.REMOVED_FROM_STAGE, onRemovedFromStage, false );
	disposeBitmap();
}

 

bitmapMode 속성을 구현할때 주의할 사항은 bitmapMode가 true일 때마다 BitmapData를 생성하는 일은 매우 비효율적이라는 것이다. 왜냐하면 이동할때마다 BitmapData를 만드는 것이기 때문이다. 그러므로 처음 이동이 발생할때만 BitmapData를 만들도록 하는게 중요하다. 또한 bitmapMode가 false라고 해서 기존에 만든 BitmapData를 삭제해서는 안된다. 삭제하면 언급한데로 다시 BitmapData를 만들어야하기 때문이다.

 

또 한가지 여기서 발생하는 가장 큰 문제는 적절한 메모리 관리이다. BitmapData를 잘 삭제해주어야 메모리 문제가 발생하지 않기 때문에다. 단순히 위에서 언급한 bitmapMode 변경으로는 BitmapData를 제대로 삭제할 수 없다. 그러므로 타일(Tile)클래스에 disposeBitmap() 함수를 만들어 bitmap.bitmapdata.dispose()를 호출하여 비트맵을 삭제하도록 하고 타일이 removeChild 될때 이 disposeBitmap()을 호출하여 BitmapData를 완벽하게 삭제하도록 해준다. removeChild가 되는 시점은 Event.REMOVED_FROM_STAGE 이벤트 처리를 하면 바로 알 수 있다.

 

삭제된다는 이야기는 메모리에서 바로 삭제된다는 것을 의미하지 않는다. Flash Player는 가비지 컬렉터가 작동한다. 즉 메모리에서 삭제될 대상을 모아두었다가 어느 시점에 한꺼번에 삭제한다. 이는 메모리 관리의 효율적인 방법중에 하나로서 개발자는 가비지 컬렉터 대상이 되도록 모든 참조를 없애주도록 해야한다. 가비지 컬렉터에 대해서는 아래 필자의 글을 참고한다.

 

[Flex / AIR / ActionScript3] Event 청취자와 메모리 관리

Flash Player 의 가비지 컬렉션(GC) 동작 방식에 대해

 

메모리가 제대로 반환이 되는지 확인하기 위해 Flex Builder(현 Flash Builder)의 프로파일링 기능을 이용하거나 System.totalMemory를 적극적으로 활용하여 메모리 상태를 점검하는것이 좋겠다.

 

소개한 대로 Bitmap과 Vector 이미지를 필요할 때마다 바꿔가면서 처리하는 것이 필자가 발견한 최상 방법이였다. 적용시 키포인트는 적절한 메모리 관리와 BitmapData를 자주 생성하지 않도록 하는 것이다.

 

 

 

DisplayObject 속성중에 cacheAsBitmap을 사용해 본 적이 있나요?

 

cacheAsBitmap은 시각화된 객체를 Bitmap으로 캐싱해주는 역할을 하는 속성입니다. 이것이 true가 되면 DisplayObject는 Bitmap처럼 되는 것이지요. Flash에서 Bitmap과 상반되는 것이 Vector입니다. 일반적으로 Flash에서 그래픽적인 요소는 거의 Vector로 처리되지요. Bitmap과 Vector에 대한 차이점과 장단점을 굳이 설명안하겠습니다.

 

단지 제가 궁금하고 해결하고 싶은 문제는 cacheAsBitmap속성이 적용된 것을 확대/축소할때 입니다.

이 속성을 true로 하고 이동처리(마우스 Drag등)을 하는 것과 안한것은 엄청난(?)속도 차이를 보입니다. 복잡한 Graphic이 들어가고 이동이 필요한 경우 cacheAsBitmap을 true로 설정하여 애플리케이션의 퍼포먼스를 높힐 수 있지요. 하지만 cachAsBitmap은 메모리와는 적입니다. 가령 확대/축소할때 가장 큰 문제가 되는데 Bitmap이 확대/축소할때 이미지는 그 크기만큼 메모리를 잡아먹습니다. 아래 제가 쓴 글을 보시면 이해될 겁니다.

 

[Flex/AIR] 메모리의 무서운 적! DisplayObject의 cacheAsBitmap 속성

 

저는 이동시에는 cacheAsBitmap을 true로 설정하고 확대/축소할때는 false로 설정해서 퍼포먼스 개선을 하려고 합니다. 하지만 cacheASBitmap을 설정하더라도 적용되는 시점이 문제입니다. 확대/축소하기 전에 false로 지정하고 바로 확대/축소를 하면 메모리를 잡아먹는다는 것이지요. 다음 Render시점에서 확대/축소를 해야하는 것인가 생각이 드는데 아직 뾰족한 묘안이 없습니다.

 

혹시 저와 같은 경험을 하셨다던가 아니면 해결책을 알고 계신분 있다면 댓글 부탁드리겠습니다.

 

 

ByteArray.org에 기존 Flash Player 9에서 사용되는 corelib의 JPGEncoder보다 더 빠른 Flash Player 10용으로 제작된 JPGEncoder를 만들어 공개했다. 더 빠를 수 있었던 것은 VectorBitmapData.setVector() 때문이다. 기존 Array 형태보다 글쓴이 컴퓨터에서는 2.5배 빠르다고 한다. 아래 프로그램에서 테스트 할 수 있다. 2880 x 2800 비트맵데이타를 encoding 하도록 만들어졌다.

 

 

 

필자는 아직 여러가지 문제로 Flash Player 10용으로 개발하지 못하고 있어 아쉽지만 위와 같은 기능을 앞으로 잘 사용하면 속도개선에 큰 도움이 될 것이라 판단이 된다.

 

+ Recent posts