구를 표현하는데 있어서 많은 3D 예제들이 그냥 경도, 위도로 나눠서 텍스쳐 입히는 것이 대부분입니다. 사실 그게 가장 쉬운 방법이고 완성된 구를 표현할 것 같으면 그편이 좋습니다.

 

Geodesic Sphere에 대한 프로그램 코드는 그리 많지 않은 것 같더군요. 대신 건축물은 왜 이렇게 많은지.. ㅎㅎ 이 형태의 구로 만든 건축물이 매우 이쁘고 독특해보이죠. 디즈니랜드의 Epcot Dom이 매우 유명한 Geodesic Sphere의 한종류이죠.

 

 

응용하면 아래처럼 의자도 만들 수 있네요. 왠지 세련되어 보입니다.

 

Geodesic이라는 말은 두점간에 최단선(측지선)을 의미합니다. 이 원리를 이용해 만든 구가 바로 Geodesic 구라는 것이지요.

 

구글 어스(Google Earth)와 같은 3D 프로그램에서 구의 형태를 어떻게 표현했다고 생각하시나요? 정확히는 모르겠지만 Geodesic Sphere 기법을 이용했을 거라 추측합니다. 구글어스에 보이는 지도는 타일 이미지들의 모임입니다. 만약 경도,위도로 그 타일 이미지를 나눠버리면 결국 극쪽(북극, 남극)쪽에 불러와야할 타일 이미지는 엄청 많아지겠죠. 확대할수록 더할 겁니다. 이런 점에 비춰볼때 3D에서 Geodesic Sphere의 선택이 필요할 것 같아집니다.

 

Geodesic Sphere를 표현하는 방법은 다양합니다. TETRAHEDRON, OCTAHEDRON, ICOSAHEDRON 기반등을 이용해 그려나갈 수 있지요. 아래처럼요.

 

출처 : http://www.geepers.co.uk/software/geodesicsphere.html

 

아직 3D 지도에 어떤 방식이 좋을지 명확히 판단하지 못했지만 OCTAHEDRON 또는  ICOSAHEDRON  기반이 아닐까 생각합니다.

 

 

회전 : 마우스 드래그

폴리곤수 조절 : 방향키 Up/Down

 

Flash 3D API의 drawTriangle 메소드 기반으로 그려본겁니다. 일반 경도, 위도만을 가지고 그릴때보다 복잡한 편이라 애를 먹었습니다. 극부분이 이미지가 찌그러져보이는 것은 UV설정이 잘못되었기 때문입니다. 이부분에 대한 보완이 필요합니다. 어쨌든 3D 지도 기반을 만들기 위한 첫단추를 꽨 셈이군요. ^^

 

소스코드 아래 링크에서 볼 수 있습니다.

http://wonderfl.net/code/73c32973543fd5d1827ae725ae92f143784b5f97 

 

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

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