3D 환경맵핑(Environment Mapping)의 한 기법으로 Cube Maps가 있다. 말그대로 직육면체의 각면에 대한 텍스쳐 이미지로 3D 환경을 조성하는 방법인데 아래 이미지를 보면 바로 이해할 수 있다.

 

Cube Map

출처 : developer.com

 

Cube Maps는 환경 맵핑 방법중 가장 빠르고 매우 사실적으로 묘사할 수 있는 방법으로 내가 알기로는 게임등에 자주 사용하는 것으로 알고  있다. 단점은 모서리 부분에서 약간 티가 난다는 점이다.

 

Flash Player 10부터 3D API를 제공한다. 이 API들을 적절히 활용하면 Papervision3D나 Away3D와 같은 라이브러리를 이용하지 않고도 어려움 없이 Cube Maps을 구현할 수 있다.

 

아래 프로그램은 Flash Player 10에서 제공하는 3D API만 이용해서 Cube Map을 구현한 것이다. 마우스 드래그로 회전이 가능하다.

 

마우스로 Drag하면 회전이 됩니다.
(Mac에서 화면이 작게 나오는데 아직 이유를 모르겠네요 ^^;)

 

위와 같은 프로그램을 만들기 위해 6면을 가지는 텍스쳐 이미지가 필요하다. 인터넷에 많이 있는데 아래처럼 Layout이 여러종류가 있다.

 

Cube Map Layout

Cube Map Layout 출처 : cgtextures.com

 

나는 가장 일반적이고 자주 쓰이는 Horizontal Cross방식 대신에 Horizontal Strip(NVidia DDS Exporter Layout)을 이용했다. 이 방식은 아래같이 구성된다.

 

Cube Map Layout 출처 : cgtextures.com

 

이러한 이미지가 어떻게 Cube Map을 만들 수 있는지 아래 화면을 보면 쉽게 알 수 있다.

 

마우스로 Drag하면 회전이 됩니다.

 

아래는 위 프로그램에 대한 소스이다.

 

 

package
{
	import flash.display.*;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.geom.*;
	import flash.utils.getTimer;
	
	[SWF(frameRate=60, backgroundColor=0x000000)]

	/**
	 * Cube Map 예제 
	 * @author Yongho, Ji (http://blog.jidolstar.com/574)
	 * @since 2009.08.05
	 */
	public class CubeSky extends Sprite
	{
		[Embed(source="sky.jpg")]
		private const SKY:Class;
		
		//투영된 Vectex 정보
		private var projected:Vector.<Number>;
		
		//World 변환 행렬 
		private var world:Matrix3D;
		
		//투영을 위한 변환행렬 
		private var projection:Matrix3D;
		
		//Mesh 데이터 
		private var mesh:GraphicsTrianglePath
				
		//Texture
		private var texture:BitmapData = (new SKY as Bitmap).bitmapData;
		
		//Viewport (3D 렌더링 대상)
		private var viewport:Shape;
		
		//Viewport의 Z축 위치 
		private var viewPortZAxis:Number = 0;		
		
		public function CubeSky()
		{
			//Viewport 
			viewport = new Shape;
			addChild( viewport );
			
			//투영 변환 행렬 
			var p:PerspectiveProjection = new PerspectiveProjection()
			p.fieldOfView = 30;
			projection = p.toMatrix3D();

			//World 변환 행렬
			world = new Matrix3D();
			
			var check3D:Vector3D = Utils3D.projectVector(
										projection, 
										new Vector3D(1.0,0,1.0)
									);			
									
			//Mesh 데이타 
			mesh = createCubeMesh( check3D.x );
			projected = new Vector.<Number>(0,false);
			viewPortZAxis = check3D.x;// * 4;
			
			//이벤트 핸들러 등록 
			stage.addEventListener( Event.RESIZE, resize );
			stage.addEventListener( Event.ENTER_FRAME, render );		
			//stage.addEventListener( MouseEvent.MOUSE_WHEEL, onMouseWheel );	
			stage.addEventListener( MouseEvent.MOUSE_DOWN, onMouseEvent );
			resize();
		}
		
		private function resize( event:Event = null ):void
		{
			viewport.x = stage.stageWidth/2;
			viewport.y = stage.stageHeight/2;	
		}		
		
		private function render( event:Event = null ):void
		{
            world.identity(); //단위행렬로 전환 
			world.appendRotation( zRotation, Vector3D.Z_AXIS );
			world.appendRotation( 90+xRotation, Vector3D.X_AXIS );
            world.appendTranslation(0, 0, viewPortZAxis); //이동 
            world.append(projection); //투영 변환 적용 
            
            // mesh 데이터를  투영하여  projected 생성 
            // uvtData도 갱신된다. 갱신되는 데이터는 T값이다.             
            Utils3D.projectVectors( world, mesh.vertices, projected, mesh.uvtData );
  
            // texture를 이용해 렌더링
            viewport.graphics.clear();
            //viewport.graphics.lineStyle( 1, 0xff0000 );
        	viewport.graphics.beginBitmapFill( texture, null, false, true );
            viewport.graphics.drawTriangles( projected, mesh.indices, mesh.uvtData, mesh.culling );
        	//Cube는  z축을 굳이 정렬할 필요 없다.
            //viewport.graphics.drawTriangles( projected, getSortedIndices(mesh), mesh.uvtData, mesh.culling );            	
		}
		
		/**
		 * 마우스 휠 처리 
		 */ 
		private function onMouseWheel( event:MouseEvent ):void
		{
			viewPortZAxis += event.delta * 10;
		}	

		private var xRotation:Number = 0;
		private var zRotation:Number = 0;
		private var prevX:Number;
		private var prevY:Number;		
		/**
		 * 마우스 이벤트 - x,y축 회전
		 */ 
		private function onMouseEvent( event:MouseEvent ):void
		{
			switch( event.type )
			{
				case MouseEvent.MOUSE_DOWN:
					stage.addEventListener( MouseEvent.MOUSE_MOVE, onMouseEvent );
					stage.addEventListener( MouseEvent.MOUSE_UP, onMouseEvent );
					prevX = mouseX;
					prevY = mouseY;
					break;
				case MouseEvent.MOUSE_MOVE:
					if( event.buttonDown == false )
					{
						stage.removeEventListener( MouseEvent.MOUSE_MOVE, onMouseEvent );
						stage.removeEventListener( MouseEvent.MOUSE_UP, onMouseEvent );
						break;
					}
					var dx:Number = mouseX - prevX;
					var dy:Number = mouseY - prevY;
					zRotation += dx;
					xRotation += dy;
					if( xRotation > 90 ) xRotation = 90;
					if( xRotation < -90 ) xRotation = -90;
					prevX = mouseX;
					prevY = mouseY;
					break;
				case MouseEvent.MOUSE_UP:
					stage.removeEventListener( MouseEvent.MOUSE_MOVE, onMouseEvent );
					stage.removeEventListener( MouseEvent.MOUSE_UP, onMouseEvent );
					break;
			}	
		}
	}
}

import flash.display.GraphicsTrianglePath;
import flash.display.TriangleCulling;

/**
 * Cube Mesh 데이타 작성 
 * @param size 한변의 사이즈
 * @return mesh 데이터 
 */ 
function createCubeMesh( size:Number = 512 ):GraphicsTrianglePath
{
	var vertices:Vector.<Number> = new Vector.<Number>( 0, false );
	var indices:Vector.<int> = new Vector.<int>( 0, false );
	var uvtData:Vector.<Number> = new Vector.<Number>( 0, false );
	var mesh:GraphicsTrianglePath = new GraphicsTrianglePath( vertices, indices, uvtData, TriangleCulling.POSITIVE );
	var s:Number = size/2;
	var w:Number = size * 6;
	var u00:Number = 0;
	var u01:Number = (size-1)/w;
	var u10:Number = size/w;
	var u11:Number = (size*2-1)/w;
	var u20:Number = (size*2)/w;
	var u21:Number = (size*3-1)/w;
	var u30:Number = (size*3)/w;
	var u31:Number = (size*4-1)/w;
	var u40:Number = (size*4)/w;
	var u41:Number = (size*5-1)/w;
	var u50:Number = (size*5)/w;
	var u51:Number = (size*6-1)/w;
	
	mesh.vertices.push( 
		//Right		
		s,-s,-s,	//0
		s,-s,s,		//1
		s,s,s,		//2
		s,s,-s, 	//3

		//Left
		-s,s,-s, 	//4
		-s,s,s, 	//5
		-s,-s,s, 	//6
		-s,-s,-s, 	//7
		
	
		//Top
		-s,-s,s, 	//8
		-s,s,s, 	//9
		s,s,s, 		//10
		s,-s,s, 	//11
		
		//Bottom
		-s,s,-s, 	//12
		-s,-s,-s, 	//13
 		s,-s,-s, 	//14
		s,s,-s, 	//15
		
		//Front
		-s,-s,-s, 	//16
		-s,-s,s, 	//17
		s,-s,s, 	//18
		s,-s,-s, 	//19

		//Back
		s,s,-s, 	//20
		s,s,s, 		//21
		-s,s,s, 	//22
		-s,s,-s 	//23
	);
	
	
	mesh.indices.push( 
		0,1,2, 0,2,3, 	//Right
		4,5,6, 4,6,7,  	//Left
		8,9,10, 8,10,11, //Top
		12,13,14, 12,14,15, //Bottom
		16,17,18, 16,18,19, //Front
		20,21,22, 20,22,23 //Back
	 );
		
	mesh.uvtData.push( 
		//Right
		u00, 1, 1,
		u00, 0, 1,
		u01, 0, 1,
		u01, 1, 1,

		//Left
		u10, 1, 1,
		u10, 0, 1,
		u11, 0, 1,
		u11, 1, 1,

		//Top
		u20, 1, 1,
		u20, 0, 1,
		u21, 0, 1,
		u21, 1, 1,

		//Bottom
		u30, 1, 1,
		u30, 0, 1,
		u31, 0, 1,
		u31, 1, 1,

		//Front
		u40, 1, 1,
		u40, 0, 1,
		u41, 0, 1,
		u41, 1, 1,

		//Back
		u50, 1, 1,
		u50, 0, 1,
		u51, 0, 1,
		u51, 1, 1
	 );

	
	return mesh;
}

/**
 * GraphicsTrianglePath를 기반으로, Z축으로 sort된 인덱스를 돌려준다.
 * 이 작업을 해주어야 z축 깊이에 따라 Triangle이 제대로 그려진다. 
 * @param mesh 정보 
 * @return sort된 index 데이터 
 */
function getSortedIndices( mesh:GraphicsTrianglePath ):Vector.<int> 
{
    var triangles:Array = [];
    var length:uint = mesh.indices.length;
    
    //z축 sort를 위한 기반 제작 
    for ( var i:uint=0; i < length; i += 3 ) 
    {
        var i1:uint = mesh.indices[ i+0 ];
        var i2:uint = mesh.indices[ i+1 ];
        var i3:uint = mesh.indices[ i+2 ];
        var z:Number = Math.min( mesh.uvtData[i1 * 3 + 2], mesh.uvtData[i2 * 3 + 2], mesh.uvtData[i3 * 3 + 2] );
        if (z > 0) 
        { 
        	triangles.push({i1:i1, i2:i2, i3:i3, z:z}); 
        }
    }
    
    //z축으로 sort
    triangles = triangles.sortOn("z", Array.NUMERIC);
    
    //sort된 값을 이용해 Vector값 만듬 
    var sortedIndices:Vector.<int> = new Vector.<int>(0, false);
    for each (var triangle:Object in triangles) 
    {
        sortedIndices.push(triangle.i1, triangle.i2, triangle.i3);
    }
    return sortedIndices;
}

 

 

아래 이미지는 위 프로그램에서 사용한 이미지이다.

 

위 프로그램에서는 Graphics.drawTriangle() 메소드를 이용했다. 하지만 이 함수를 사용하지 않고도 만들수 있다. 왜냐하면 Flash Player 10부터는 DisplayObject에 대해 x,y,z축 회전 및 이동 API가 추가되었기 때문이다. 그러면 위 프로그램 소스처럼 힘들게 vertex, index, uvt 데이타를 만들지 않아도 될 것이다.

 

내 생각에 Flash에서 3D 개발을 위해 Graphics.drawTriangle()와 DisplayObject의 3D API, Matrix3D 등을 서로 섞어가면서 만드는 것이 좋을 것 같다.

 

개발환경 : Flash Builder 4 Beta 1 (Flash CS4 에서도 개발할 수 있음.)

 

참고내용

 

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

 

+ Recent posts