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)

 

토성(Saturn)

 

Papervision3D나 Away3D 라이브러리를 이용하지 않고 Flash Player 10 3D API만을 이용해 태양계의 행성인 토성을 그려보았다. 토성을 그리기 위해 먼저 표면부분과 고리부분의 Texture를 구해야한다. 아래 사이트에서 다양한 행성의 Texture 그림을 구할 수 있다.

 

http://planetpixelemporium.com/saturn.html

 

토성의 고리 Texture

토성의 표면 Texture

 

Texture를 이용해 3D로 만들기 위한 아이디어는 아래 링크를 참고한다.

 

[Flash 3D]우리 아가 회전시키기 2-Texture를 이용해 여러모양 만들기

[Flash 3D]우리 아가 회전시키기-UV맵핑,원근투영 이용

 

 

토성 그리기 : 표면과 고리가 겹치는 문제

 

지구와 같은 행성과 다르게 토성의 경우에는 고리가 있다. 토성의 표면은 그냥 구형태로 Mesh데이타를 만들어 처리하면 된다. 하지만 고리의 경우는 다른 형태로 Mesh데이타를 만들어야 한다. 설명은 생략하겠다.

 

아래는 아래 예시의 소스이다.

invalid-file

고리와 구가 겹치는 문제가 있는 소스

 

토성을 모양을 만들기 위해 토성의 표면과 고리에 대한 Mesh데이타를 만들어야 한다. 아래코드는 고리(Ring) 부분의 Mesh데이타를 만드는 부분이다. 표면부분과 다르게 고리 Texture를 반복해서 그리는 데이타를 만든다.

/**
 * 고리 Mesh 데이터 작성 
 * @param radius 고리 안쪽 반지름  
 * @param thickness 고리 두께
 * @param div 고리 조각수 
 * @return mesh 데이터 
 */
function createRingMesh( radius:Number, thickness:Number, div:uint ):GraphicsTrianglePath
{
    var vertices:Vector. = new Vector.( 0, false );
    var indices:Vector. = new Vector.( 0, false );
    var uvtData:Vector. = new Vector.( 0, false );
    var mesh:GraphicsTrianglePath = new GraphicsTrianglePath( vertices, indices, uvtData, TriangleCulling.NONE );
    var s:Number = 0;
    var x1:Number = radius;
    var x2:Number = radius + thickness;
    var y1:Number = 0;
    var y2:Number = 0;
    var z:Number = 0;          
    var cos:Number;
    var sin:Number;    
    var n:Number = 0;
    
    for( var i:uint = 0; i < div; i++ )
    {
        //Vertices
        mesh.vertices.push( 
            x1, y1, z, 
            x2, y2, z 
        );
        s = Math.PI * 2 * (i + 1) / div;
        cos = Math.cos( s );
        sin = Math.sin( s );
        x1 = radius * cos;
        x2 = (radius + thickness) * cos;
        y1 = radius * sin;
        y2 = (radius + thickness) * sin;
        mesh.vertices.push( 
            x1, y1, z, 
            x2, y2, z 
        );
        //UVT
        mesh.uvtData.push( 
            0,1,1, 
            1,1,1, 
            0,0,1, 
            1,0,1 
        );
        
        //Indices
        n = i * 4;
        mesh.indices.push( n, n+1, n+2, n+2, n+1, n+3 );             
    }    
    
    return mesh;
}

 

Enterfame시에 렌더링을 실시하는데 Utils3D.projectVectors(), graphics.beginBitmapFill(), graphics.drawTriangles() 메소드를 2번씩 호출한다. 즉, 토성의 구와 고리를 그리기 위해 투영, 렌더링을 2번씩 하는 것이다.

private function onEnterFrame( event:Event ):void
{
    world.identity(); //단위행렬로 전환 
    world.appendRotation( getTimer() * 0.0027, Vector3D.Z_AXIS );
    world.appendRotation( xRotation, Vector3D.X_AXIS );
    world.appendRotation( yRotation, Vector3D.Y_AXIS );
    world.appendTranslation(0, 0, viewPortZAxis); //이동 
    world.append(projection.toMatrix3D()); //투영 변환 적용 
    
    // mesh 데이터를  투영하여  projected 생성 
    // uvtData도 갱신된다. 갱신되는 데이터는 T값이다. 
    Utils3D.projectVectors( world, saturnFaceMesh.vertices, saturnFaceProjected, saturnFaceMesh.uvtData );
    Utils3D.projectVectors( world, saturnRingMesh.vertices, saturnRingProjected, saturnRingMesh.uvtData );    
    
    viewport.graphics.clear();
    // Triangle 라인을 그림 
    if( visibleTriangle )
    {
        viewport.graphics.lineStyle( 1, 0xff0000, 0.1 );
    }
    
    //Texture 입힌다.
    viewport.graphics.beginBitmapFill( saturnFaceTexture, null, false, true );
    viewport.graphics.drawTriangles( saturnFaceProjected, getSortedIndices(saturnFaceMesh), saturnFaceMesh.uvtData, saturnFaceMesh.culling );                
    viewport.graphics.beginBitmapFill( saturnRingTexture, null, false, true );
    viewport.graphics.drawTriangles( saturnRingProjected, getSortedIndices(saturnRingMesh), saturnRingMesh.uvtData, saturnRingMesh.culling );                
}

 

언뜻보면 이 코드는 문제가 없어보이지만 실제로 실행해보면 바로 문제가 있다는 것을 확인할 수 있다.

아래는 이 프로그램을 실행한 것이다.

 

 

마우스로 돌려보고 화살표키로 확대/축소도 할 수 있다. 또한 Space키 누르면 삼각형 부분이 보인다.

 

실행한 결과를 보면 문제가 있다. 토성 표면 뒤에 고리가 보인다. 당연히 저렇게 그려지면 안된다. 이 문제는 위의 방법처럼 Texture와 Mesh데이타가 2개여서는 방법이 나오지 않는다. 왜냐하면 graphics.drawTriangles() 함수를 2번 그려주게 되면 먼저 그린 것은 항상 나중에 그린것에 의해 가려지기 때문이다.

 

 

토성 그리기 : 고리 겹치지 않게 만들기

 

invalid-file

표면과 고리가 겹치지 않는 소스

 

위처럼 고리가 토성표면에 겹치는 문제를 해결하기 위한 방법은 Texture를 하나로 통합하고 Mesh데이타도 하나로 만든다. 그리고 graphics.drawTriangles() 를 한번만 호출하도록 하면 된다.

 

아래 함수는 고리, 표면 Texture를 1개의 텍스쳐로 만들어준다.

/**
 * 텍스쳐 생성 
 * @param faceTexture 표면의 BitmapData
 * @param ringTexture 고리의 BitmapData 
 * @return TexturInfo의 객체 
 */ 
function createTexture( faceTexture:BitmapData, ringTexture:BitmapData ):TextureInfo
{
	var texture:BitmapData;

	texture = new BitmapData( 
			Math.max( faceTexture.width, ringTexture.width ), 
			faceTexture.height + ringTexture.height, 
			true,
			0xff000000 
	);

	faceTexture.lock();
	ringTexture.lock();

	var facePixels:ByteArray ;
	var ringPixels:ByteArray;
	facePixels = faceTexture.getPixels( new Rectangle( 0, 0, faceTexture.width, faceTexture.height ) );
	ringPixels = ringTexture.getPixels( new Rectangle( 0, 0, ringTexture.width, ringTexture.height ) );
	facePixels.position = 0;
	ringPixels.position = 0;

	texture.setPixels( 
		new Rectangle( 0, 0, faceTexture.width, faceTexture.height ), 
		facePixels 
	);
	texture.setPixels( 
		new Rectangle( 0, faceTexture.height, ringTexture.width, ringTexture.height ), 
		ringPixels 
	);
	
	var faceRect:Rectangle = new Rectangle( 0, 0, faceTexture.width, faceTexture.height );
	var ringRect:Rectangle = new Rectangle( 0, faceTexture.height+1, ringTexture.width, ringTexture.height );
	
	faceTexture.unlock();
	ringTexture.unlock();
	
	return new TextureInfo( texture, faceRect, ringRect ); 
}

 

결국 아래 그림처럼 Texture BitmapData가 만들어진다.

 

위 함수에 TextureInfo는 실제 texture Bitmap정보와 Texture상의 표면과 고리 부분에 대한 영역정보를 가지고 있다. 이 영역정보를 가지고 Mesh 데이타를 만들도록 한다.

 

/**
 * 토성모양의 Mesh 데이타 
 * @param textureInfo 텍스쳐 정보 
 * @return Mesh 데이타 
 */ 
function createMesh( textureInfo:TextureInfo ):GraphicsTrianglePath
{
	var width:Number = textureInfo.texture.width;
	var height:Number =textureInfo.texture.height;
	var faceRect:Rectangle = textureInfo.faceRect;
	var ringRect:Rectangle = textureInfo.ringRect;

	var minU:Number;
	var maxU:Number;
	var minV:Number;
	var maxV:Number;
	
	//표면 Mesh 데이타 만들기 
	minU = faceRect.x / width;
	maxU = (faceRect.x + faceRect.width) / width;
	minV = faceRect.y / height;
	maxV = (faceRect.y + faceRect.height) / height;
	var faceMesh:GraphicsTrianglePath = createSphereMesh( 50, 24, 24, minU, maxU, minV, maxV );
	
	//고리 Mesh 데이타 만들기 
	minU = ringRect.x / width;
	maxU = (ringRect.x + ringRect.width) / width;
	minV = ringRect.y / height;
	maxV = (ringRect.y + ringRect.height) / height;
	var ringMesh:GraphicsTrianglePath = createRingMesh( 70, 20, 50, minU, maxU, minV, maxV );

	//고리 Mesh 데이타에서 Index 부분 조정 
	var deltaIndex:uint = faceMesh.vertices.length/3; //Vertex는 x,y,z 3개씩 묶이므로... ^^
	var length:uint = ringMesh.indices.length;
	for( var i:int = 0; i < length; i++ )
	{
		//아래와 같이 2개의 mesh가 합쳐지면 뒤에 붙는  ring부분의 index값이 변경되야한다.
		ringMesh.indices[i] += deltaIndex; 
	}
	
	//최종 Mesh 데이타 완성 
	var mesh:GraphicsTrianglePath = new GraphicsTrianglePath();
	mesh.vertices = faceMesh.vertices.concat( ringMesh.vertices );
	mesh.uvtData = faceMesh.uvtData.concat( ringMesh.uvtData );
	mesh.indices = faceMesh.indices.concat( ringMesh.indices );
	mesh.culling = TriangleCulling.NONE;
	return mesh;	
}

 

위 함수는 2개의 Mesh데이터를 이용해 1개의 Mesh데이타로 통합해준다. 이로써 이제는 투영, 렌더링을 한번만 하도록 한다.

 

private function onEnterFrame( event:Event ):void
{
	world.identity(); //단위행렬로 전환 
	world.appendRotation( getTimer() * 0.0027, Vector3D.Z_AXIS );
	world.appendRotation( xRotation, Vector3D.X_AXIS );
	world.appendRotation( yRotation, Vector3D.Y_AXIS );
	world.appendTranslation(0, 0, viewPortZAxis); //이동 
	world.append(projection.toMatrix3D()); //투영 변환 적용 
	
	// mesh 데이터를  투영하여  projected 생성 
	// uvtData도 갱신된다. 갱신되는 데이터는 T값이다. 
	Utils3D.projectVectors( world, mesh.vertices, projected, mesh.uvtData );
	
	viewport.graphics.clear();

	// Triangle 라인을 그림 
	if( visibleTriangle )
	{
		viewport.graphics.lineStyle( 1, 0xff0000, 0.1 );
	}
	
	//Texture 입힌다.
	viewport.graphics.beginBitmapFill( textureInfo.texture, null, false, true );
	viewport.graphics.drawTriangles( projected, getSortedIndices(mesh), mesh.uvtData, mesh.culling );            	

}

 

위 함수처럼 1번만 투영/렌더링 처리하면 이제 토성의 표면과 고리는 아래처럼 자연스럽게 나오게 된다.

 

 

하지만 완벽하지 않다. 이유는 렌더링시 Culling을 None으로 지정했기 때문이다. 이는 보이지 않는 표면까지 그린다는 의미이다. 고리의 경우 양쪽면이 다보여야한다. 그러므로 고리의 Mesh데이타는 None으로 지정한다. 하지만 토성 표면의 경우는 화면 앞쪽을 향하는 부분만 보이면 되므로 Culling을 Positive로 지정하면 된다. 첫번째 예제에서는 어짜피 Mesh 데이터가 2개이므로 상관없었지만 두번째의 경우에는 양면 다보여야하는 고리때문에 토성의 표면도 None처리 했다. 이는 보이지 않는 부분까지 렌더링하므로 그만큼 속도저하가 일어난다. 이를 해결하기 위해 Positive로 지정하되 고리를 앞뒤면으로 겹쳐서 그리면 된다. 하지만 이 방법밖에 없는지 의문이다.

 

 

정리하기

 

이런 작업은 신나는 일이다. ^^

 

이 토성에 Light효과를 주어 Shading 예제도 만들어봐야 겠다.

 

참고로 모든 작업은 Flash Builder 4에서 했으며 Flex 4 SDK를 이용했다. 결과물은 Flash Player 10이상 설치된 브라우져에서만 확인할 수 있다.

 

위 코드는 아래 링크에서도 볼 수 있다.

 

Saturn 3D, 토성, 고리 중첩 해결못한 것

Saturn 3D, 토성, 고리 중첩 해결한 것

 

참고사이트

[Flash 3D]우리 아가 회전시키기 2-Texture를 이용해 여러모양 만들기

[Flash 3D]우리 아가 회전시키기-UV맵핑,원근투영 이용

 

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

 

이전에 [Flash 3D]우리 아가 회전시키기-UV맵핑,원근투영 이용 라는 제목으로 3D Texture 맵핑을 이용하는 기초적인 예제를 설명했다. 이 글은 이를 응용해 실제 3D 모양을 만들어 Texture를 입히는 예제 보여준다.

 

Adobe Flash Player 10이상 부터 3D 관련 API를 제공하고 있다. 그러므로 Papervision3D, Away3D와 같은 기존 3D라이브러리를 이용하지 않아도 충분히 3D 구현이 가능해졌다.

 

이번에도 예진이가 모델이 되어주었다. 3D Texture 모델 이쁜 공쥬~~ 훗~

 

하지만 모델의 운명은 가혹하다는....

 

아래는 예제 프로그램의 캡쳐화면이다.

 

원기둥과 원환체(도너츠 모양)를 Mesh 데이터로 삼았다.

프로그램 상에서 키보드 1을 누르면 원기둥으로 키보드 2를 누르면 원환체로 바뀐다.

 

원기둥 모양

 

원환체 모양

 

화면위에 마우스를 클릭하면 렌더링된 삼각형이 그려진다. 이것으로 Texture가 어떻게 입혀졌는지 한눈에 확인할 수 있다.

 

원기둥 모양

원환체

 

키보드 T를 누르면 Texture가  입혀지지 않고 아래처럼 형태만 보여준다.

 

마우스 휠을 이용하면 z축으로 확대/축소가 가능하다.

 

Flash Player 10 API 만으로도 이런 것을 만들 수 있다는데 어쩌면 감동을 받을지도.... 나 또한 그랬다.

 

다음은 위 예제 프로그램이다. Flash Builder 4나 Flash CS4에서 ActionScript 3.0 프로젝트를 생성해서 만들면 되겠다.

 

package
{
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.GraphicsTrianglePath;
	import flash.display.Shape;
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageQuality;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.events.MouseEvent;
	import flash.geom.Matrix3D;
	import flash.geom.PerspectiveProjection;
	import flash.geom.Utils3D;
	import flash.geom.Vector3D;
	import flash.utils.getTimer;
	
    [SWF(frameRate=60, backgroundColor=0x000000)]
	
	/**
	 * 3D Texture 예제.
	 * @author Yongho, Ji
	 * @since 2009.07.01
	 * @see http://help.adobe.com/ko_KR/ActionScript/3.0_ProgrammingAS3/WSF24A5A75-38D6-4a44-BDC6-927A2B123E90.html
	 */ 
	public class Texture3D extends Sprite
	{
		[Embed(source="mybaby.png")]
		private var ImageClass:Class;
		
		// 투영된  Vertex 정보 
		private var projected:Vector.<Number>;

        // 투영
        private var projection:PerspectiveProjection = new PerspectiveProjection();
        
        // World 변환 행렬 
        private var world:Matrix3D = new Matrix3D();
		
		// Viewport (3D 렌더링 대상)
		private var viewport:Shape = new Shape();
		
		// Mesh 데이터 
		private var mesh:GraphicsTrianglePath
		
		// Texture
		private var texture:BitmapData = (new ImageClass() as Bitmap).bitmapData;
		
		//triangle을 보여줄지 여부  
		private var visibleTriangle:Boolean = false;
		
		//Texture를 보여줄지 여부 
		private var visibleTexture:Boolean = true;
		
		//실린더  Mesh번호 
		private const MESH_CYLINDER:int = 1;
		
		//원형체  Mesh번호  
		private const MESH_TORUS:int = 2;
		
		//선택한 Mesh 번호 
		private var selectedMesh:int;
		
		//Viewport의 Z축 위치 
		private var viewPortZAxis:Number = 300;
		
		/**
		 * 생성자 
		 */ 
		public function Texture3D()
		{
			super();
			
			//화면 설정 
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.quality = StageQuality.BEST;
			
			//viewport를 화면의 중심으로 
			viewport.x = stage.stageWidth/2;
			viewport.y = stage.stageHeight/2;
			addChild(viewport);
			
			//projection의 fieldOfView를  60으로 지정 
			projection.fieldOfView = 60;
			
			//mesh 데이터 설정 
			selectMesh( MESH_CYLINDER );
			
			//이벤트 처리 							
			stage.addEventListener( Event.RESIZE, onResize );
			addEventListener( Event.ENTER_FRAME, onEnterFrame );
			stage.addEventListener( MouseEvent.CLICK, onMouseClick );
			stage.addEventListener( KeyboardEvent.KEY_DOWN, onKey );
			stage.addEventListener( MouseEvent.MOUSE_WHEEL, onMouseWheel );
		}
		
		/**
		 * Mesh를 선택한다.
		 */ 
		private function selectMesh( meshNumber:int ):void
		{
			if( selectedMesh == meshNumber ) return;
			selectedMesh = meshNumber;
			
			//투영결과 Vertex 데이타를 초기화시킨다. 
			//이것을 해야 Mesh데이터가 바뀔때마다 drawTriangle에서 에러 발생 안함 
			projected  = new Vector.<Number>(0, false);
			switch( selectedMesh )
			{
				//실린더 
				case MESH_CYLINDER:
					mesh = createCylinderMesh( 50, 100, 20, 5 );
					break;
				//원형체 
				case MESH_TORUS:
					mesh = createTorusMesh( 50, 25, 32, 16 );
					break;
			}			
		}
		
		/**
		 * 사이즈 변경시 처리  
		 */ 
		private function onResize( event:Event ):void
		{
			//viewport는 항상 화면의 중심에 위치하도록 처리 
			viewport.x = stage.stageWidth/2;
			viewport.y = stage.stageHeight/2;	
		}
		
		/**
		 * 프레임 마다 처리. 
		 */ 
		private function onEnterFrame( event:Event ):void
		{
            world.identity(); //단위행렬로 전환 
            world.appendRotation( getTimer() * 0.027, Vector3D.X_AXIS ); //X축 회전
            world.appendRotation( getTimer() * 0.061, Vector3D.Y_AXIS ); //Y축 회전 
            world.appendTranslation(0, 0, viewPortZAxis); //이동 
            world.append(projection.toMatrix3D()); //투영 변환 적용 
            
            // mesh 데이터를  투영하여  projected 생성 
            // uvtData도 갱신된다. 갱신되는 데이터는 T값이다. 
            Utils3D.projectVectors( world, mesh.vertices, projected, mesh.uvtData );
            
            // texture를 이용해 렌더링
            viewport.graphics.clear();
            
            // Triangle 라인을 그림 
            if( visibleTriangle )
            {
            	viewport.graphics.lineStyle( 1, 0xff0000, 1.0 );
            }
            
            //Texture 입힌다.
            if( visibleTexture )
            {
            	viewport.graphics.beginBitmapFill( texture, null, false, true );
                viewport.graphics.drawTriangles( projected, getSortedIndices(mesh), mesh.uvtData, mesh.culling );            	
            }
            else
            {
            	viewport.graphics.beginFill( 0x00ccff, 1.0 );
                viewport.graphics.drawTriangles( projected, getSortedIndices(mesh), null, mesh.culling );            	
	            viewport.graphics.endFill();			
            }
		}
		
		/**
		 * 마우스 클릭 처리 
		 */ 
		private function onMouseClick( event:MouseEvent ):void
		{
			//삼각형  선 Visible 바꿈 
			visibleTriangle = !visibleTriangle;	
		}
		
		/**
		 * 키보드 이벤츠 처리  
		 */ 
		private function onKey( event:KeyboardEvent ):void
		{
			if( event.charCode == 116 )
			{
				visibleTexture = !visibleTexture;	
			}
			else
			{
				selectMesh( event.charCode - 48 ); //1,2...
			}
		}
		
		/**
		 * 마우스 휠 처리 
		 */ 
		private function onMouseWheel( event:MouseEvent ):void
		{
			viewPortZAxis += event.delta * 10;
		}
	}
}

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

/**
 * 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;
}

/**
 * 원통모양의 Mesh 데이터 작성 
 * @param radius 원통의 반지름
 * @param height 원통의 높이 
 * @param hDiv 반지름 방향으로 조각 수 
 * @param vDiv 높이 방향의 조각수 
 * @return mesh 데이터 
 */ 
function createCylinderMesh( radius:Number, height:Number, hDiv:uint, vDiv:uint ):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.NONE );
	
	for( var i:uint = 0; i <= hDiv; i++ )
	{
		var theta:Number = Math.PI * 2 * i / hDiv; //z축에서 y축으로 잰각 
		for( var j:uint = 0; j <= vDiv; j++ )
		{
			var x:Number = -height/2 + j * height/vDiv;
			var y:Number = radius * Math.sin( theta );
			var z:Number = radius * Math.cos( theta );
			mesh.vertices.push( x, y, z );				//하나의 Vertex 데이타 
			mesh.uvtData.push( i / hDiv, j / vDiv, 1 ); //하나의 UVT 데이타 . Texture의 점과 Vectex를 일치시켜 Texture를 입히기 위한 데이타이다.
			if( j < vDiv && i < hDiv )
			{
                var a:uint =  i      * (vDiv + 1) + j;
                var b:uint = (i + 1) * (vDiv + 1) + j;
                mesh.indices.push(a, a + 1, b, b + 1, b, a + 1); //삼각형의 index이다. culling방향 고려 
			} 
		}
	}
	return mesh;
}

/**
 * 원환체(도너츠모양) Mesh 데이터 작성 
 * @param hRadius 원환체의 수평축 반지름
 * @param vRadius 원환체의 수직축 반지름  
 * @param hDiv 수평 방향의 조각 수 
 * @param vDiv 높이 방향의 조각수 
 * @return mesh 데이터 
 */
function createTorusMesh( hRadius:Number, vRadius:Number, hDiv:uint, vDiv:uint ):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.NONE );
	for (var i:uint=0; i<=hDiv; i++) 
	{
		var s1:Number = Math.PI * 2 * i / hDiv;
		for (var j:uint=0; j<=vDiv; j++) 
		{
            var s2:Number = Math.PI * 2 * j / vDiv;
            var r:Number = Math.cos(s2) * vRadius + hRadius;
			var x:Number = Math.cos(s1) * r;
			var y:Number = Math.sin(s1) * r;
			var z:Number = Math.sin(s2) * vRadius;            
            mesh.vertices.push( x, y, z );	
            mesh.uvtData.push(i / hDiv, j / vDiv, 1);
            if (j < vDiv && i < hDiv) {
                var a:uint =  i      * (vDiv + 1) + j;
                var b:uint = (i + 1) * (vDiv + 1) + j;
                mesh.indices.push(b, a + 1, a, a + 1, b, b + 1);
            }
        }
    }
    return mesh;
}

 

아래는 위 프로그램을 실행한 것이다. Flash Player 10이 브라우져에 설치되어야 제대로 볼 수 있겠다.

 

 

키 1, 2 - 형태바꾸기

키 T(영문) - Texture 입히기 여부

마우스 클릭 - 삼각형 보이기 여부

마우스 휠 - 확대/축소

 

위 프로그램에서 사용된 GraphicsTrianglePath는 2D 공간에 3D 기하 도형 렌더링을 지원하기 위해 사용되는 클래스이다. 특별한 기능은 없지만 3D 데이터를 다룰 때 index, vertex, uvt 등의 데이타를 모두 담을 수 있기 때문에 사용하면 유용하다.

 

위 프로그램에서 원뿔, 구에 대한 mesh 함수를 추가하면 더 좋은 예제가 될 것이라 생각한다. 또한 광원(light)처리까지 곁들이면 금상첨화일 것이다.

 

 

참고글

 

 

+ Recent posts