이제 그래픽으로 화면을 그려보려 한다. 다음 위치에 있는 Cube 샘플을 먼저 빌드해서 실행해 보도록 하자.
C:\cygwin\usr\local\pspdev\psp\sdk\samples\gu\cube
cube 샘플을 실행시키면 아래 그림과 같이 정육면체가 나오고 빙글빙글 돌고 있을 것이다. 그냥 밋밋한 정육면체가 아니라 PSPDEV.ORG라는 글씨가 그려져 있는 텍스쳐까지 적용되어 있는데, 여기서는 일단 텍스쳐는 제외하고 vertex 정의로 정육면체를 그리는 것에만 촛점을 맞추어서 보기로 한다. 또한, 그래픽 샘플들은 대부분 common 디렉토리에 있는 callbacks.c와 vram.c를 함께 링크하고 있다. callbacks.c는 매번 샘플들에서 반복되던 exit callback 코드들이 들어있고, vram.c에 대해서는 천천히 살펴보도록 한다.
cube.c에서는 먼저 pspgu.h와 pspgum.h 헤더 파일을 include하고 있다. pspgu.h에는 PSP의 그래픽 시스템을 사용하는데 필요한 상수와 함수의 프로토타입이 들어 있고, pspgum.h에는 3D 그래픽의 행렬 계산과 관련된 상수와 함수의 프로토타입이 들어 있다.
그리고 Vertex라는 타입을 다음과 같이 정의하고 있다.
struct Vertex
{
float u, v;
unsigned int color;
float x,y,z;
};
u와 v는 텍스쳐와 관련된 것이니 일단 넘어가고, color 값과 x, y, z 좌표를 지정할 수 있도록 구성되어 있다. 하나의 삼각형 폴리곤을 만들기 위해서는 3개의 vertex가 필요하고, 사각형을 만들기 위해서는 4개의 vertex가 필요하다. vertex list를 지정할 때 사각형을 표현하기 위해 여러가지 방법이 있지만 여기서는 삼각형 2개를 사용해서 사각형을 표현하는 방법을 사용하고 있다. 다음 코드는 12개의 삼각형을 지정해서 총 12개의 삼각형, 6개의 사각형으로 정육면체를 표현한다.
struct Vertex __attribute__((aligned(16))) vertices[12*3] =
{
{0, 0, 0xff7f0000,-1,-1, 1}, // 0
{1, 0, 0xff7f0000,-1, 1, 1}, // 4
{1, 1, 0xff7f0000, 1, 1, 1}, // 5
{0, 0, 0xff7f0000,-1,-1, 1}, // 0
{1, 1, 0xff7f0000, 1, 1, 1}, // 5
{0, 1, 0xff7f0000, 1,-1, 1}, // 1
{0, 0, 0xff7f0000,-1,-1,-1}, // 3
{1, 0, 0xff7f0000, 1,-1,-1}, // 2
{1, 1, 0xff7f0000, 1, 1,-1}, // 6
{0, 0, 0xff7f0000,-1,-1,-1}, // 3
{1, 1, 0xff7f0000, 1, 1,-1}, // 6
{0, 1, 0xff7f0000,-1, 1,-1}, // 7
{0, 0, 0xff007f00, 1,-1,-1}, // 0
{1, 0, 0xff007f00, 1,-1, 1}, // 3
{1, 1, 0xff007f00, 1, 1, 1}, // 7
{0, 0, 0xff007f00, 1,-1,-1}, // 0
{1, 1, 0xff007f00, 1, 1, 1}, // 7
{0, 1, 0xff007f00, 1, 1,-1}, // 4
{0, 0, 0xff007f00,-1,-1,-1}, // 0
{1, 0, 0xff007f00,-1, 1,-1}, // 3
{1, 1, 0xff007f00,-1, 1, 1}, // 7
{0, 0, 0xff007f00,-1,-1,-1}, // 0
{1, 1, 0xff007f00,-1, 1, 1}, // 7
{0, 1, 0xff007f00,-1,-1, 1}, // 4
{0, 0, 0xff00007f,-1, 1,-1}, // 0
{1, 0, 0xff00007f, 1, 1,-1}, // 1
{1, 1, 0xff00007f, 1, 1, 1}, // 2
{0, 0, 0xff00007f,-1, 1,-1}, // 0
{1, 1, 0xff00007f, 1, 1, 1}, // 2
{0, 1, 0xff00007f,-1, 1, 1}, // 3
{0, 0, 0xff00007f,-1,-1,-1}, // 4
{1, 0, 0xff00007f,-1,-1, 1}, // 7
{1, 1, 0xff00007f, 1,-1, 1}, // 6
{0, 0, 0xff00007f,-1,-1,-1}, // 4
{1, 1, 0xff00007f, 1,-1, 1}, // 6
{0, 1, 0xff00007f, 1,-1,-1}, // 5
};
주석에 써있는 숫자는 정육면체의 각 꼭지점을 나타낸다. 좌표들을 종이에 하나씩 그려보면 이 배열이 나타내는 의미를 확실하게 알게 될 것이다. 더불어서 하나의 삼각형을 표현할 때 꼭지점을 나열하는 순서도 의미가 있으니 눈여겨 보도록 한다.
32비트 부호없는 정수인 color 값은 앞에서부터 8비트씩 Alpha, Blue, Green, Red를 의미한다. 혹은 GU_COLOR( float red, float green, float blue, float alpha ) 매크로를 사용해도 된다. GU_COLOR 매크로를 사용한다면 값을 0.0f에서 1.0f 사이로 주어야 한다.
__attribute__((aligned(16))) 가 들어가는 이유는 PSP의 그래픽 처리 프로세서가 데이터를 16bit align된 상태로 받아들이기 때문이다. 앞으로 텍스쳐 데이터나 vertex 데이터 모두 이와 같이 16bit로 align해서 사용될 것이다.
main 함수를 보면 VRAM 버퍼링에 관련된 몇가지 처리 후 다음과 같은 초기화 코드가 있다. 초기화 코드들을 하나 하나 살펴보도록 한다.
sceGuInit();
sceGuStart(GU_DIRECT,list);
그래픽 시스템을 초기화하고 새 display context를 바로 바로 렌더링하도록 시작한다.
sceGuDrawBuffer(GU_PSM_8888,fbp0,BUF_WIDTH);
sceGuDispBuffer(SCR_WIDTH,SCR_HEIGHT,fbp1,BUF_WIDTH);
sceGuDepthBuffer(zbp,BUF_WIDTH);
draw buffer, display buffer, depth buffer를 설정한다. draw buffer는 폴리곤들이 렌더링될 버퍼이고, display buffer는 실제 화면에 표시되는 버퍼이고, depth buffer는 폴리곤들의 순서를 저장하는 버퍼이다.
sceGuOffset(2048 - (SCR_WIDTH/2),2048 - (SCR_HEIGHT/2));
sceGuViewport(2048,2048,SCR_WIDTH,SCR_HEIGHT);
sceGuDepthRange(65535,0);
sceGuOffset은 가상 좌표의 offset을 설정한다. PSP의 가상 좌표는 4096x4096 크기이므로 여기서는 중간 위치가 되도록 offset을 설정하고 있다. 그리고 viewport의 좌표로 2048, 2048로 정가운데를 주고 크기로 PSP의 해상도를 주고 있다. depth buffer의 계산을 어느 정도까지 할 것인가를 지정하는 sceGuDepthRange에서는 전 depth를 모두 검사하도록 범위를 최대와 최소로 주고 있다.
sceGuScissor(0,0,SCR_WIDTH,SCR_HEIGHT);
sceGuEnable(GU_SCISSOR_TEST);
sceGuDepthFunc(GU_GEQUAL);
sceGuEnable(GU_DEPTH_TEST);
sceGuFrontFace(GU_CW);
sceGuShadeModel(GU_SMOOTH);
sceGuEnable(GU_CULL_FACE);
sceGuEnable(GU_TEXTURE_2D);
sceGuEnable(GU_CLIP_PLANES);
그 다음은 렌더링 방식에 대한 갖가지 설정을 해주고 있는데, 화면에 있지 않은 폴리곤은 렌더링하지 않도록 scissor test를 설정해 주고, depth buffer를 사용해서 뒤쪽에 있어 가려지는 폴리곤은 렌더링하지 않도록 depth test를 설정해 주고, culling을 사용해서 폴리곤의 앞면만 렌더링하도록 하고 있다. 정육면체이므로 외부면만 렌더링하면 되고 내부면은 절대 보여지지 않을 것이니 culling으로 렌더링하지 않도록 하는 것이다. 어느쪽이 렌더링되는가는 처음 폴리곤을 표현할 때 지정한 꼭지점의 순서가 앞에서 보았을 때 시계방향인 곳이 렌더링되는 쪽이다.
sceGuFinish();
sceGuSync(0,0);
sceDisplayWaitVblankStart();
sceGuDisplay(GU_TRUE);
그리고 display context에 대한 설정을 마치게 된다.
sceGumMatrixMode(GU_PROJECTION);
sceGumLoadIdentity();
sceGumPerspective(75.0f,16.0f/9.0f,0.5f,1000.0f);
좀 더 밑으로 내려가보면 projection matrix를 설정하는 부분이 있다. 원근감이 있도록 투영할 것이기 때문에 sceGumPerspective로 75.0f를 주고 0.5f와 1000.0f로 투영할 범위를 지정해 주고 있다. sceGumLoadIdentity 함수는 현재 matrix를 초기로 돌려주는 역할을 한다. 즉, 위 코드는 projection matrix를 선택해서 초기화 한 후 원근감을 가지도록 해주고 어느 영역을 렌더링해서 투영할 것인지를 설정한다.
sceGumMatrixMode(GU_MODEL);
sceGumLoadIdentity();
{
ScePspFVector3 pos = { 0, 0, -2.5f };
ScePspFVector3 rot = { val * 0.79f * (GU_PI/180.0f), val * 0.98f * (GU_PI/180.0f), val * 1.32f * (GU_PI/180.0f) };
sceGumTranslate(&pos);
sceGumRotateXYZ(&rot);
}
여기서는 model matrix를 선택해서 초기화 한 후 sceGumTranslate로 이동하고 sceGumRotateXYZ로 회전시키고 있다. val 변수값은 루프를 돌면서 하나씩 증가해 나가므로 rot 값, 즉 회전하는 정도가 프레임 단위 별로 변화하므로 최종 결과는 정육면체가 돌게 보이는 것이다. 아직 정육면체를 그리지 않았다는 것을 눈치 챘다면 일단 정육면체를 그리기 전에 정육면체의 좌표값들에 변화가 가해지고 있다는 것을 알게 될 것이다. 내부적으로는 translate matrix, rotate matrix, 정육면체 matrix들을 모두 곱하는 연산이 이루어지게 된다.
sceGumDrawArray(GU_TRIANGLES,GU_TEXTURE_32BITF|GU_COLOR_8888|GU_VERTEX_32BITF|GU_TRANSFORM_3D,12*3,0,vertices);
텍스쳐는 일단 건너뛰고, 위 코드가 실행되면 정육면체를 그리게 되는데 GU_TRIANGLES로 우리의 vertex 배열이 삼각형 폴리곤으로 이루어진 것이라는 것을 알려주고, 모델의 특성을 지정해 준 후, 배열의 크기를 알려준다. GU_TRIANGLES가 있다면 당연히 다른 것들도 있을 것이므로 폴리곤을 표현하는 방법은 GU_TRIANGLES_STRIP, GU_TRIANGLES_FAN 등과 같이 여러가지가 있다.
3D 그래픽에 대한 상세한 설명은 생략하였다. 이 부분은 상세히 들어가면 들어갈수록 방대한 양이 될 것이므로 3D 그래픽을 처음 접하는 사람은 3D 그래픽에 대해 다룬 다른 책을 한번 읽어보는 것이 큰 도움이 될 것이다. 응용으로 샘플에 있는 여러가지 값들을 하나하나 바꿔가면서 그 결과가 어떻게 변해가는가도 해보도록 하자.
댓글