토성(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)

 

+ Recent posts