Article

Gamza 2011. 5. 19. 12:26



시대도 시대이니만큼 CPU의 픽셀연산을 몽땅 버리고, 블렌딩/확대/축소/필터링/멀티텍스쳐링/컬러쉐이딩등 수많은 하드웨어 가속기의 능력을 사용해 보려고 마음을 먹었습니다. 그런데 열받는것이....D3D로 2D출력을 해보니...이넘이 색상이 뭉개지는것 같기도 하고, 한 텍셀씩 밀린다거나 끝쪽에 한라인이 삐져나오는등...벼라별 문제가 다 일어나는겁니다.

모처럼만에 큰맘먹고 3D가속기를 써보려던 마음이 마구 흔들리는 순간입니다.
이럴때 구원의 숫자 -0.5가 등장하죠.
D3D로 2D출력할때 x,y좌표에서 0.5를 빼주면 희한하게도 깔끔하게 나오는것을 보게되는데요.
여기선 그 증상과 이유를 간단히 설명드리려고 합니다.
"난 -0.5 같은거 안해도 잘만나오는데.." 라고 하시는 분들은...반드시 읽어주세요.

1. 잘못된 구현
D3D로 2D를 표현하려고 할때 십중팔구는 아래처럼 잘못된 구현을 하게됩니다.
void DrawTexture( long x, long y, long w, long h, loat tu, float tv, float tw, float th )
{
    ... 
    Vertex[0].position.x = (float)x;
    Vertex[0].position.y = (float)y;
    Vertex[1].position.x = Vertex[0].position.x + w;
    Vertex[1].position.y = Vertex[0].position.y;
    Vertex[2].position.x = Vertex[0].position.x + w;
    Vertex[2].position.y = Vertex[0].position.y + h;
    Vertex[3].position.x = Vertex[0].position.x;
    Vertex[3].position.y = Vertex[0].position.y + h;
    Vertex[0].tu = tu;
    Vertex[0].tv = tv;
    Vertex[1].tu = Vertex[0].tu + tw;
    Vertex[1].tv = Vertex[0].tv;
    Vertex[2].tu = Vertex[0].tu + tw;
    Vertex[2].tv = Vertex[0].tv + th;
    Vertex[3].tu = Vertex[0].tu;
    Vertex[3].tv = Vertex[0].tv + th;
    ...
이제부터 위코드가 어째서 잘못된 구현인지 알아보도록 하겠습니다.
(위코드에서 Vertex는 D3DFVF_XYZRHW포맷이던, 직교투영행렬을 사용하는 D3DFVF_XYZ포맷이던 상관없음.)

2. 증상
8x8 bmp 한장을 읽어서 1:1 크기로 렌더링을 해봅니다.
(한장의 그림을 그냥 렌더링한것과 좌우뒤집기, 상하 뒤집기, 상하좌우 뒤집기를 합니다.)
// filter off
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_MAGFILTER, D3DTEXF_POINT );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_MINFILTER, D3DTEXF_POINT );
DrawTexture(  1, 1, 8, 8, 0, 0,  1,  1 );// 정상출력.
DrawTexture( 10, 1, 8, 8, 1, 0, -1,  1 );// 좌우뒤집기.
DrawTexture(  1,10, 8, 8, 0, 1,  1, -1 );// 상하뒤집기.
DrawTexture( 10,10, 8, 8, 1, 1, -1, -1 );// 상하좌우뒤집기.
// filter on
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_MAGFILTER, D3DTEXF_LINEAR );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_MINFILTER, D3DTEXF_LINEAR );
DrawTexture( 20+ 1, 1, 8, 8, 0, 0,  1,  1 );// 정상출력.
DrawTexture( 20+10, 1, 8, 8, 1, 0, -1,  1 );// 좌우뒤집기.    
DrawTexture( 20+ 1,10, 8, 8, 0, 1,  1, -1 );// 상하뒤집기.    
DrawTexture( 20+10,10, 8, 8, 1, 1, -1, -1 );// 상하좌우뒤집기.
그 결과는.....
오...잉..?...이게 왠일입니까?
왜이렇게 된거죠?
필터링하지 않고 뒤집기를 하지않은넘은 그래도 잘 나온것 같은데....이럴 경우엔 문제가 없는걸까요?
천만에 말씀....이넘도 사실 잘 나오는척 하는것 뿐이고...스케일링을 해보면 그 잘못됨이 만천하에 뽀록이 나버립니다.
(자신의 코드가 위와 같았다면...각자 자기코드를 가지고 실험을 해봅시다.)

3. 원인
D3D의 버텍스는 실수좌표를 사용합니다. 하지만 스크린좌표는 픽셀좌표뿐이죠.
2D/3D상관없이 스크린 좌표는 원래 정수밖에 없습니다.
그런데 2D에서는 정수로 지정해 주던 좌표를, 3D에서는 실수로 지정해줘야 하는거죠.
별로 문제될것이 없다고 생각하시겠지만....위에서 보신 문제가 바로 이것때문입니다.
그림에 나와있는것 처럼 스크린의 좌측상단의 1x1영역 전체가 픽셀좌표(0,0) 입니다.
D3D는 여기에 실수좌표인 Vertex(0,0)을 찍으라고 시키면....1x1영역의 중앙에 찍습니다.
스크린 좌표에서는 1x1영역전체가 (0,0)이니 상당히 자연스러운 결과입니다.
그런데 이게 뭐가 문제가 되냐고요?
문제는 3D는 2D와는 달리 텍스쳐 좌표로 텍셀좌표가 아닌 실수좌표를 사용한다는겁니다.
왼쪽에 필터링을 하지 않은 그림을 보시죠... 좌표(0,0)은 무슨색일까요? 애매모호하죠?....
이건 사실 정하기 나름인데, 운좋게도 D3D에서는 좌표(0,0)을 빨간색으로 취급합니다.
좌표(0,0)은 간신히...턱걸이로 빨간색의 반열(?)에 오른셈이죠.
이젠 오른쪽에 필터링된 그림을 봅시다. 이건 아주 쉽게 이해가 됩니다. 텍스쳐 좌표(0,0)은 주변 네개의 색상을 섞어놓은 색상이고, 순수한 텍스쳐 색상을 사용하려면 0.5텍셀위치 를 찍어주어야 합니다.
...그럼 이제 우리가 했던짓을 재현해 보도록 하겠습니다.
픽셀좌표(0,0)에 버텍스(0,0)을 찍는데 텍스쳐 좌표(0,0)을 맵핑 하기 !
자...이제 처음에 나온결과가 이해가 되셨겠죠?
이해가 안되시는 분들은.....그림을 찬찬히 잘 뜯어보시면 이해가 되실겁니다....^.^;;

4. 해결책
이문제의 해결하려면 픽셀과 텍셀이 일치하도록 만들어 주어야 합니다. 1x1픽셀한칸에 1x1텍셀하나가 정확히 매핑되도록 만들면 되는거죠.
방법이 생각나십니까?
두가지나 생각이 나신다고요? 오호....멋지십니다....
아직 생각이 나지 않으신 분들을 위해 설명을 드리자면...
첫째는, 텍스쳐좌표에 오프셋을 살짝걸어주어서, (0,0)을 맵핑하는게 아니고 진짜색상이 있는곳...그러니까...(0.5,0.5)텍셀위치를 맵핑해주는겁니다.
두번째는, 그와 반대로 버텍스좌표에 오프셋을 걸어주는 방식이죠.

5. 텍스쳐에 오프셋걸어주기
자...이것은...이렇게 하면 될것같습니다.
UV = (텍셀좌표 + 0.5f) / Texture_Size
float textureoffset = 0.5/8;
DrawTexture( ..., 0+textureoffset, 0+textureoffset,  1,  1 );// 정상출력.
DrawTexture( ..., 1-textureoffset, 0+textureoffset, -1,  1 );// 좌우뒤집기.
DrawTexture( ..., 0+textureoffset, 1-textureoffset,  1, -1 );// 상하뒤집기.
DrawTexture( ..., 1-textureoffset, 1-textureoffset, -1, -1 );// 상하좌우뒤집기.
오호...아주 그럴싸하군요...필터링을 켜도 깔끔하게 아주 잘 나오네요...
그런데 이방법엔 치명적인 문제가있습니다.
텍스쳐에 오프셋을 걸어버렸기때문에 확대를 하게되면 텍셀의 절반이 날라가버리는겁니다. 뿐만아니라 보너스로 사각형 영역의 마지막에는 쓸데없는 반텍셀이 덧붙게되죠.
결국 이방법은 사용할수가 없습니다.

6. 버텍스좌표에 오프셋걸어주기
결론부터 말하자면 이겁니다.
XY = (픽셀좌표 - 0.5f)
    ... 
    Vertex[0].position.x = (float)x - 0.5f;
    Vertex[0].position.y = (float)y - 0.5f;
    ...
이렇게 하면 뒤집기/확대축소/필터링등...무얼해도 전혀 문제가 없습니다.
(궁금하면 따져보세요......저한테 따지지는 마시고..^^;; )
위에서는 D3DFVF_XYZRHW를 사용하였지만, D3DFVF_XYZ+직교투영행렬의 경우도 마찬가지로 정수좌표(x,y)에서 0.5를 빼주면 됩니다. 단, D3DFVF_XYZ+직교투영행렬을 쓸때는 직접 빼주지 않고, 투영행렬앞에 -0.5를 해주는 행렬을 곱해주면 버텍스는 그냥 정수좌표를 사용해도 되겠죠.

7. 마법의 숫자 0.5
"나는 캐릭터좌표를 실수로 관리하는데, 그럼 그냥 캐릭터 좌표를 사용하면 되나?" 라는 의문을 가지신분도 있으실 겁니다. 딱잘라 말씀드리자면 대답은 NO 입니다.
실수좌표를 가졌다 하더라도 반드시 정수로 변환한다음 0.5를 빼주어야 합니다.
Vertex.XY = ( (long)(xy) - 0.5f)
이유는 텍셀과 픽셀이 정확하게 일치하지 않으면 언제든지 문제가 발생할수 있기때문입니다. 여기서는 극단적으로 캐릭터가 정수로 딱떨어지는 좌표를 갖는경우를 위에서 보여드렸지만, (정수-0.5)가 아닌 다른숫자는 모두 마찬가지 입니다.
(이문제는 OpenGL에서도 마찬가지 입니다. 임의의 소수점위치에 사각형을 찍다보면 반드시 원하지 않는 이상한 현상이 튀어나옵니다. 0.0에서부터 0.1씩 좌표를 늘려가며 뿌려보시면 바로 알수 있습니다.)
텍셀과 픽셀이 정확히 일치되는 버텍스좌표는 (정수픽셀좌표 - 0.5) 뿐이고, 이때는 무슨짓을 하더라도 2D에서 했던것고 똑같은 결과를 얻을수 있습니다.
단 텍스쳐좌표는 오프셋이 걸리지 않았다는 전제하에 말이죠.
Vertex.UV = 정수텍셀좌표 / TextureSize


이것으로 D3D로 2D출력할때 스크린좌표에서 0.5를 빼주 이유와 중요성에 대해 말씀드렸습니다.
이것으로 모두들 깔끔하고 완전한 2D출력루틴을 갖게되었으면 좋겠네요. 그럼 모두들 즐프하세요.
감자 성수올림...@~
♡달링 알랍♡



고맙습니다. 여태 이유를 몰라서 해결을 못했었는데
드디어 텍스쳐 밀림 현상을 해결했네요.
좋은글잘봤습니다 혹시 텍스쳐의 크기는 2의 배수여야 하나요?