copyrightⓒ 김성완(kaswan) [2000년 1월 12일]
OpenGL과 Direct3D의 파이프라인은 기본적인 원리에서는 같지만...
소프트웨어 엔진과 더불어 두 API도 동시에 지원할 경우 두 API의 렌더링 파이프라인의 차이점을 잘 알고 있어야 합니다.
3차원 엔진은 기본적으로 크게 세가지 모듈로 구성이 됩니다.
Transformation, Lighting, Rasterizer
이 중에서도 특히 Transformation 모듈의 Pipeline을 잘 이해해야 합니다.
OpenGL과 D3D의 파이프라인을 비교해 보죠.
OpenGL : ModelView ==> Projection ==> Viewport -> Perspective Division Direct3D : World -> View ==> Projection ==> Perspective Division -> Viewport
==> View coordinats
==> Clipping coordinats
두 파이프라인을 비교해 보면 당장에 차이가 눈에 띠는데, 실질적으로 본다면 같은 겁니다.
* 먼저 Projection Transformation 이후 단계를 살펴보면 서로 순서가 다른데..
Homogeneous 좌표 w로 나누는 Perspective Division은 먼저 하던지 나중에 하던지 순서에 상관없이 최종 결과는 동일합니다.
그래서 실질적으로는 같은 겁니다.
D3DIM 의 예제중에 DrawPrim 이라는 예제가 있는데..
D3D의 세가지 버텍스 포맷을 모두 사용해서 Primitive를 렌더링하는 예입니다.
그 중에 D3DTLVERTEX를 사용하는 Cube 모델의 경우 Transformation 을 직접하게 되는데, 소스를 살펴보면 D3D의 순서대로 하질 않고 OpenGL처럼 Perspevtive Division을 먼저하고 Viewport Transformation 을 합니다.
결국 순서에 상관이 없는 것이죠.
(참고로 그 예제 소스에는 약간의 실수가 있더군요... 어차피 간단한 예제라 텍스츄어매핑없이 단순히 쉐이딩만 하는 거라서 실행중에 드러나지 않았지만, RHW 에다 1/w 를 대입해야하는데.. w를 바로 대입했더군요. 그리고 또 Projection Transformation후에 해야하는 3D Clipping이 생략되어 있습니다. 제대로 하려면 이것도 직접 해주어야죠.)
* 두번째 차이가 나는 곳은 Projection 변환하기 전인데...
살펴보면 OpenGL은 ModelView Transformation Matrix 하나만 설정하면 되는데...
D3D는 World 와 View 두가지로 나누어서 하게 되어 있죠.
D3D에서 두개로 나누어서 하더라도 내부적으로는 어차피 하나로 합쳐지게 되므로 결국 OpenGL처럼 한번에 설정하나 D3D처럼 따로 설정하나 같은 것이죠.
(D3D 매뉴얼에도 권고하기를 World TM과 View TM을 개별적으로 설정하기 보다는 하나는 항등행렬로 설정하고 하나에다 둘을 미리 곱해서 넣어주는게 속도상 유리하다고 되어있어요. 대개 3차원 엔진들은 행렬이 항등행렬인지 체크해서 아예 곱하는 계산에서 제외시키는 식으로 최적화 되어있기 때문이죠.)
결론적으로 OpenGL과 D3D의 Transformation 파이프라인은 동일하다고 보아도 별 문제가 없다는 것이죠.
물론 두API가 사용하는 좌표계가 서로 틀리고
OpenGL : 오른손 좌표계
Direct3D : 왼손 좌표계
좌표값을 나타내는 행렬의 경우 OpenGL의 경우 열벡터를 사용하고 D3D의 경우 행백터를 사용하기 때문에 구체적인 행렬의 곱셈순서가 서로 반대이고, 행렬이 서로 행과 열이 바뀐(Transpose) 형태로 되어 있어 구체적인 행렬값은 틀리지요.
하지만 projection 변환까지 거치고 나면 두 API모두 좌표값이 사실상 같아집니다.
무슨 얘기인고 하니 둘다 결국은 3D 클리핑을 수행하기 위한 일종의 표준적인 정규 좌표계로 변환 되어서 같은 좌표가 된다는 것이죠.
그래서 소프트웨어로 Transformation 을 처리할 경우 두 API 좌표계와 행렬 표시의 차이점을 개의치 않고. 적당한 하나의 공통 좌표계와 공통 행렬로 Projection 변환까지 수행한 후 두 API에 좌표값을 정규화된 좌표값으로 공급해주면 나머지는 동일하게 처리되는 것이죠.
OpenGL의 경우 ModelView 행렬과 Projection 행렬을 둘다 항등행렬로 설정하고 행렬연산을 직접하면 임의로 마음에 드는 좌표계를 사용하면서도 까다로운 3D 클리핑과 최종적인 ransterization을 API에게 맡길 수가 있습니다.
문제는 OpenGL의 경우 유연하게 이런식으로 하는게 먹히는데..
D3D의 경우 파이프라인의 어느 부분을 사용할 지를 Vertex 포멧으로 선택할 수 있는데.. 이 선택이 아래 세가지 밖에 없다는 것이죠.
D3DVERTEX : Transformation, Lighting, Rasterization 을 모두 D3D에게 맡김 D3DLVERTEX : Lighting, Rasterization 을 D3D에게 맡김
D3DTLVERTEX : Rasterization 만 D3D에게 맡김
위에서 보다시피 OpenGL처럼 Transformation 파이프라인의 중간에 끼어 들거나 할 수 없다는 것이죠.
즉, D3D의 경우는 Transformation을 모두 직접하거나 아니면 모두 API에 맡기거나 하는 양단의 선택밖에 없다는 것이죠.
(이런 걸 보아도 OpenGL이 얼마나 잘 만들어진 API인지 알 수 있죠.)
결국 이렇게되면 D3D의 경우 Transforamation을 직접 하려고 한다면, 까다로운 3D 클리핑까지 직접 해야 합니다.
물론 이미 소프트웨어 엔진이 제대로 구축되어 있다면 3D클리핑 도 직접 수행하므로 별문제가 없지만서도...
만약 소프트웨어 엔진없이 3D가속 카드 전용으로 만들면서 OpenGL과 D3D API를 동시에 지원하려고 한다면, 3D클리핑 처럼 힘든일은 3D API에게 맡기면서 가능한 공통의 코드를 공유할 수 있도록 하는 게 좋겠지요.
이런 경우 적당한 전략은 좌표계를 D3D에 맞추고 행렬도 D3D용을 공용으로 사용하고, 조명계산도 직접 공동으로 처리하고...
OpenGL의 경우 Projection 변환까지는 직접 처리하고, 나머지는 OpenGL에 맡기고..
D3D의 경우 D3DLVERTEX를 사용해서 좌표변환 계산은 통째로 D3D에 맡기는 겁니다.
그럼 3D 클리핑도 알아서 해주겠지요...