dev.log2009. 2. 17. 00:00
행렬을 구현하는 데에 있어서 가장 어려운 것은, 바로 메모리상의 배열 우선순위(major ordering)를 정하는 것이다. 단위 배열로 행벡터를 쓸 것인가 열벡터를 쓸 것인가를 결정하는 것인데, 행벡터는 행렬에 있어서 같은 행번호를 가진 원소가 서로 인접하도록 메모리 상에 배치하는 것이고, 열벡터는 같은 열 번호를 가진 원소가 서로 인접하도록 메모리 상에 배치하는 것. 어차피 둘 중의 하나 밖에 선택할 수 없기 때문에 심플한 문제이기는 한데, 좀 미묘한 문제가 걸려 있다.

우선, 행우선(row major) 배치는, 다음을 보자.


이와 같은 행렬이 행 우선 배열에서는 메모리 상에 선형으로 다음과 같이 배치된다.

 a00 a01 a02 a03 a10 a11 a12 a13 a20 a21 a22 a23 a30 a31 a32 a33

이는 C/C++의 2차원 배열 선언과 동일한 배치이다.

float a[4][4]

위와같은 선언은 메모리를 다음과 같이 배치한다.

a[0][0] a[0][1] a[0][2] a[0][3] a[1][0] a[1][1] a[1][2] a[1][3] a[2][0] a[2][1] a[2][2] a[2][3]
... a[3][0] a[3][1] a[3][2] a[3][3]

따라서, 배열의 첨자가 그냥 순서대로 [행번호][열번호]가 된다는 편리함이 있다. 행렬의 이름이 '행', '열' 순서이므로 이름과 잘 일치한다. 또한 C/C++의 기본 배열과 개념상 일치해 보인다는 장점이 있고, Direct3D에서 사용하는 방식이므로 별다른 변환 없이 Direct3D에서 사용이 가능하다. 유혹적이다.

반면, 열우선(column major) 배치는 행렬을 메모리에 배열하는 방법이 반대다.

 a00 a10 a20 a30 a01 a11 a21 a31 a02 a12 a22 a32 a03 a13 a23 a33

이를 2차원 배열로 표현한다면 [열번호][행번호]의 순서로 첨자를 부여해야 한다는 점이 C/C++의 표준과 불일치한다는 인상을 준다. 하지만 어차피 가로/세로의 선호는 사람에 따라 달라지는 것이므로 열이 앞이냐 행이 앞이냐 하는 문제는 표준에 정의되어 있지 않은 문제이다. 메모리를 가로 방향으로 나열해서 그렇지, 다음과 같이 나열하면 자연스러워 보인다.

 a00
 a10
 a20
 a30
 a01
 a11
 a21
 a31
 a02
 a12
 a22
 a32
 a03
 a13
 a23
 a33

이는 전적으로 메모리가 위아래로 뻗어있느냐 양옆으로 뻗어있느냐로 보는 것의 차이인데, 사실 어느 쪽을 택해도 무방하다. 물론 행렬을 표현한 2차원 배열에서 첨자의 순서가 '행','열' 순서가 아닌 점은 약간 불편할 것 같긴 하다. 하지만 흔히 쓰는 수학적인 (벡터를 세로로 적고 행렬의 뒤에 곱하는) 표현과 일치한다는 장점이 있고, OpenGL에서 사용하는 오더링이므로 OpenGL에서 별다른 변환 없이 사용이 가능하다.

ice 프로젝트의 요구조건중 하나가 OpenGL과 Direct3D의 동시 지원이었기 때문에, 가능한 방법은 3가지다
1. 행 우선 배치로 구현하고, OpenGL에서는 일일이 transpose하여 로드한다.
2. 열 우선 배치로 구현하고, Direct3D에서는 일일이 transpose하여 로드한다.
3. 둘 다 구현한다.
사실, 둘 다 구현하는게 바람직하긴 한데, 문제는 그렇게 할 경우에 클래스가 2개 생기므로 이름 공간이 더럽혀지는 결과가 초래되는 점이 아름답지 못하다. row_matrix와 col_matrix가 공존하는 구조를 용납할 수는 없는 법.

그래서 전에 벡터 클래스를 만들 때 SSE 스페셜라이제이션을 템플릿 매개변수로 컨트롤했던 것을 약간 이용하여, 역시 템플릿 매개변수로 major ordering을 결정하는 방법을 취하기로 했다.
enum SPECIALIZE_POLICY
{
    SPECIALIZE_DEFAULT,
    SPECIALIZE_SSE,
};

enum MAJOR_ORDER
{
    ROW_MAJOR,
    COLUMN_MAJOR,
};

// OpenGL용 열우선 배열을 기본으로 구현
template <class scalar_t, MAJOR_ORDER order = COLUMN_MAJOR, SPECIALIZE_POLICY policy = SPECIALIZE_DEFAULT >
struct matrix4t
{
    enum { MAJOR_VECTOR = order };
    typedef scalar_t                    scalar_type;
    typedef vector4t<scalar_t, policy>  vector_type;
    typedef vector3t<scalar_t, policy>  coord_type;
    union
    {
        vector_type v[4];
        scalar_type s[16];
    } m;
public:
    matrix4t() {}
    matrix4t(   scalar_type _11, scalar_type _12, scalar_type _13, scalar_type _14,
                scalar_type _21, scalar_type _22, scalar_type _23, scalar_type _24,
                scalar_type _31, scalar_type _32, scalar_type _33, scalar_type _34,
                scalar_type _41, scalar_type _42, scalar_type _43, scalar_type _44 )
                :m.v[0](_11,_21,_31,_41),
                 m.v[1](_12,_22,_32,_42),
                 m.v[2](_13,_23,_33,_43),
                 m.v[3](_14,_24,_34,_44)
    {
    }
    template <class scalar>
    explicit matrix4t( const scalar* a )
        :m.v[0](a[0], a[4], a[8], a[12] ),
         m.v[1](a[1], a[5], a[9], a[13] ),
         m.v[2](a[2], a[6], a[10],a[14]),
         m.v[3](a[3], a[7], a[11],a[15])
    {
    }
};

// Direct3D용 행 우선 배열을 위한 부분 스페셜라이제이션
template < class scalar_t, SPECIALIZE_POLICY policy >
struct matrix4t< scalar_t, ROW_MAJOR, policy >
{
    enum { MAJOR_VECTOR = ROW_MAJOR };
    typedef scalar_t                    scalar_type;
    typedef vector4t<scalar_t, policy>  vector_type;
    typedef vector3t<scalar_t, policy>  coord_type;
    union
    {
        vector_type v[4];
        scalar_type s[16];
    } m;

   
    static const matrix4t zero;
    static const matrix4t identity;
public:
    matrix4t() {}

    matrix4t(   scalar_type _11, scalar_type _12, scalar_type _13, scalar_type _14,
                scalar_type _21, scalar_type _22, scalar_type _23, scalar_type _24,
                scalar_type _31, scalar_type _32, scalar_type _33, scalar_type _34,
                scalar_type _41, scalar_type _42, scalar_type _43, scalar_type _44 )
                :m.v[0](_11,_12,_13,_14),
                 m.v[1](_21,_22,_23,_24),
                 m.v[2](_31,_32,_33,_34),
                 m.v[3](_41,_42,_43,_44)
    {
    }

    template <class scalar>
    explicit matrix4t( const scalar* a )
        :m.v[0](a[0], a[1], a[2], a[3] ),
         m.v[1](a[4], a[5], a[6], a[7] ),
         m.v[2](a[8], a[9], a[10],a[11]),
         m.v[3](a[12],a[13],a[14],a[15])
    {
    }
};

// SSE 템플릿 매개변수는 명시적으로 스페셜라이제이션할 경우에만 허용하도록 막아둠
template< class scalar_t, MAJOR_ORDER order >
struct matrix4t< scalar_t, order, SPECIALIZE_SSE >
{
    scalar_t error[0]; // SSE spcecialization is not allowed unless explicit
};

쓸 때는 어떤 오더링을 쓸 것인지와 SSE 스페셜라이제이션을 쓸 것인지를 명시하면 된다.

matrix4t<float, COLUMN_MAJOR, SPECIALIZE_DEFAULT >

물론, 위와 같이 쓰는 것은 불편하므로, OpenGL 모듈에서는 다음과 같이 타입으로 선언하는 것이 편리.

[render_ogl.h]
typedef matrix4t<float, COLUMN_MAJOR, SPECIALIZE_DEFAULT > matrix4;

물론, Direct3D모듈에서도 비슷하게 타입으로 선언할 수 있다.

[render_d3d.h]
typedef matrix4t<float, ROW_MAJOR, SPECIALIZE_DEFAULT > matrix4;

구현은 두개를 다 해야 하니 빡세도, 쓸때는 골라쓰는 재미가 있을듯; ice 프로젝트의 철학은 '클라이언트 측에서 엔진의 구성요소를 고를 수 있게 한다'는 것이므로.

Posted by uhm