'3D프로그래밍'에 해당되는 글 2건

  1. 2009.02.17 행렬 구현
  2. 2009.02.03 벡터는 벡터일 뿐
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
dev.log2009. 2. 3. 22:19
벡터는 벡터일 뿐, 3차원 벡터는 좌표 3개가 아니다.
3D게임 프로그래밍을 시작하는, 특히 2D게임을 만들다가 3D로 전향하려는 사람들에게서 잘 드러나는 것 같은데, 벡터를 벡터로 보지 않고 계산해야 할 좌표 3개로 보는 경향이 있는 것 같다. 사실, 벡터를 조작하여 얻고자 하는 무언가는 대부분의 경우 주어진 벡터로부터 특정한 방향을 가진 벡터, 혹은 주어진 벡터의 길이와 관계된 것일 경우가 많다. 이런 것을 구하기 위해서는 벡터를 좌표 3개로 분할할 필요가 없는데도, 습관처럼 좌표를 쪼개서 계산한다면, 벡터 연산에 익숙하지 않기 때문일 것 같다.

3차원 공간상의 벡터에 정의된 연산은 대략 반전, 덧셈, 뺄셈, 실수 곱셈, 실수 나눗셈, 그리고 내적과 외적이 거의 전부다. 이를 적절히 이용하면 간략한 표현으로 원하는 결과를 얻을 수 있는데, 좌표로 쪼개서 계산하면 고려해야할 예외적 상황이 증가하여 오히려 코드가 복잡해지는 결과가 초래되곤 한다.

오늘 본 문제는 '어떤 점에서 선분에 내린 수선의 발이 그 선분을 분할하는 비율'이다. 조금 더 상세히 정의하자면 선분의 시작점 s, 끝점 t가 주어졌을때, 임의의 점 p에서 내린 수선의 발 f에 대해서 |sf|/|st|의 값을 구하라는 것이다. 이런 문제는, 고등학교 수학시간에도 나오는 문제이지만, 이를 좌표로 쪼개서 풀려고 다음과 같이 하는 것을 보았다. (정확히 기억하고 있진 않을 수도 있지만)
  1. 각 벡터 p, s, t를 한 평면, 이를테면 xy평면에 투영하여 p', s', t'으로 한다.
  2. xy평면에서 선분s't'의 방향은 ( tx-sx, ty-sy )이다.
  3. xy평면에서 선분s't'과 직교하는 선분의 방향 d는 ( -ty+sy, tx-sx )이다.
  4. p'의 s'에 대한 변위 o를 구한다. 이때 o( p'x-s'x, p'y-s'y )이다.
  5. 점 o를 지나고 d와 평행한 직선과 선분 s't'의 교점 q를 연립방정식으로 구하면 그 점이 수선의 발.
  6. qx/(tx-sx)가 구하고자 하는 값이다.

대충 돌아가기는 하겠지만, 몇가지 예외사항이 존재한다. 우선 과정1에서 선분st가 xy평면에 수직일 경우 다른 평면을 골라 투영해야 한다. 또한 6단계에서 선분s't'이 y축에 평행하면 0/0의 꼴이 되어 부정이 되므로 역시 다른 좌표축을 골라야 한다. 여러가지 복잡한 예외를 고려해야 하기 때문에 결과적으로는 코드 길이가 원래보다 2배 정도 길어지게 마련이다. 하지만, 이는 내적의 성질을 이용해서 다음과 같은 간략한 방법으로 구할 수 있다.

이렇게 하면 선분의 방향이 어떠하든 일반적인 해를 구할 수 있다. 내적은 두 벡터의 길이의 곱에 사잇각의 코사인을 곱한 것과 같다는 성질을 이용한 것인데, 벡터의 길이에 코사인을 곱하면 다른 쪽 벡터에 투영된 길이가 나온다는 점을 생각하면 되겠다.

물론, 이런 방법으로는 길이의 비만 구할 수 있기 때문에 수선의 발 자체가 필요한 경우에는 따로 계산을 해야 하겠지만, 그때에도 위 결과로부터 직접 구할 수 있다. 선분 ts와 선분 fs의 길이의 비를 이미 구했으므로, 선분 ts를 이 비율대로 곱하기만 하면 수선의 발이 직접 구해진다. 따라서 수선의 발 f는 다음과 같이 구할 수 있다.

벡터에 정의된 연산이 몇개가 안되므로, 성급하게 벡터를 좌표로 쪼개려고 하는 것보다는, 벡터 연산으로 요리조리 조작해 보는 편이 더 일반적으로 유효한 결과를 가져온다. 3D프로그래밍의 시작은 벡터를 좌표의 집합이 아닌, 벡터 그 자체로 보는 것이라고나.


Posted by uhm