dev.log2009. 2. 7. 18:33
일상적으로 통용되는 단위가 두가지 이상이라면 매우 헷갈린다. 미국은 미터법(metric system)과 영국단위(imperial system)의 차이 때문에 우주선[각주:1]을 하나 날려먹은 적도 있다. 그만큼은 심각한 것은 아니지만 회사에서 K군이 카메라 FOV를 조정할 때 라디안(radian)과 도(degree)의 차이 때문에 무척 헷갈려 하는 걸 본 적도 있다.

라디안은 우아하기 때문에 수학자나 공학자, 물리학자들이 좋아하고, 도는 역사가 오래기 때문에 세상 다른 모든 사람들이 선호하는 각도의 단위이다. 하지만 세상 모든 사람들이 우아하지 못한 단위를 사용한다고 해서 우리네 프로그래머까지 그런 단위를 써야만 한다는 것은 어불성설이라는 굳은 믿음을 갖고, 심심하던 차에 각도 클래스를 만들어 봤다.

기본 요구사항은 2가지.
  1. 어떤 각도를 나타내는 값이 도인지 라디안 인지가 항상 명확해야 한다.
  2. 도와 라디안의 변환은 인간이 신경을 쓰지 않도록 자동적으로 이루어져야 한다.
1번 요구사항은 그냥 angle = 3.5f; 라고 했을 때, 이 3.5가 도인지 라디안인지는 코드만 보고는 알 수 없기 때문에 혼동의 여지를 준다는 뜻이다. 이를테면, angle = radian(3.5f); 같은 식으로 이루어지면 항상 명확할 수 있다.
2번 요구사항은, 라디안을 받아들이는 함수에 실수로 도 단위로 90.f라는 값을 넣더라도, 1번에 의해 어떤 단위가 사용되고 있는지 항상 명시되므로, 자동으로 변환돼야 인간의 실수가 오류를 불러일으키지 않을 수 있다는 것.

const float pi = 3.14159265358979323846f;                           // 물론, float 타입은 이렇게 큰 정밀도를 저장하지 못한다.
const float pi_half = 3.14159265358979323846f*0.5f;

class radian;
class degree;

class radian
{
public:
    radian() {}
    explicit radian( float v ) : _value( v ) {}
    radian( const degree& r );

    operator float() const { return _value; }
    radian& operator=( float f ) { _value = f; return *this; }
    radian& operator=( const degree& d );

private:
    float _value;
};

class degree
{
public:
    degree() {}
    explicit degree( float v ) : _value( v ) {}
    degree( const radian& r );

    operator float() const { return _value; }
    degree& operator=( float f ) { _value = f; return *this; }
    degree& operator=( const radian& r );

private:
    float _value;
};


radian::radian( const degree& d )
{
    _value = static_cast<float>(d) * pi / 180.f;
}

radian& radian::operator=( const degree& d )
{
    _value = static_cast<float>(d) * pi / 180.f;
    return *this;
}

degree::degree( const radian& r )
{
    _value = static_cast<float>(r) * 180.f / pi;
}

degree& degree::operator=( const radian& r )
{
    _value = static_cast<float>(r) * 180.f / pi;
    return *this;
}

각 클래스의 float을 받아들이는 생성자가 explicit인 이유는, 함수 인자로 그냥 float 값을 넣을 때 어떤 단위를 의도하는지를 명시할 것을 강제하기 위해서이다. explicit이 아니라면 다음과 같은 일이 일어난다.

class radian
{
public:
    radian() {}
    radian( float v ) : _value( v ) {}
};

void rotate( radian r ); // radian을 받아들이는 함수
....
rotate( 90.f );              // 인간이 90도를 의도하며 함수를 호출하면 원치 않는 동작

하지만 float 생성자를 explicit으로 선언함으로써, 위와 같은 의도가 명확치 않은 코드는 아예 컴파일 단계에서 제거해 버릴 수가 있다. 컴파일 에러를 통해서;

class radian
{
public:
    radian() {}
    explicit radian( float v ) : _value( v ) {}
};

void rotate( radian r ); // radian을 받아들이는 함수
....
rotate( 90.f );              // ERROR!!!
rotate( radian(90.f) );   // 이렇게 하면 동작하지만 코드를 씀과 동시에 이상함을 발견하게 된다.
rotate( degree(90.f) );  // 이것이 원하는 동작.

그 밖의 경우에는 그냥 자연스럽게 동작한다.

radian a( pi/3.f );  // pi/3 을 지정
degree b = a;       // 도로 변환하면 60도
b = 90.f;               // 90도를 지정
a = b;                  // 라디안으로 변환하면 pi/2

이런 방법은 약간만 잘 이용하면 m/ft, ms/s, kg/pound 등등 다른 단위가 이용될 수 있는 모든 부분에서 인간의 헷갈림에 의한 실수를 원천봉쇄할 수 있지 않을까 싶다;

  1. Mars Climate Orbiter는 단위계의 차이로 파괴된 우주선이다. 추력을 설정할 때 지상의 운영 프로그램은 파운드 단위를, 기체의 제어 프로그램은 kg 단위를 기준으로 제작되어 발생한 사고. [본문으로]
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
geek.log2009. 2. 2. 00:57
블로그 유입경로를 보다 보니 이런게 눈에 띄었다.


어허허허허허허

물론, deque는 dequeue와 동일한 대상을 가리키는 것이 맞지만, 사전을 찾아보면, deque의 발음은 [데크]이고, dequeue의 발음이 [디큐]이다. 그리고, STL에서 double-ended queue를 구현할때는 이름을 dequeue라 붙이지 않고, deque라 붙였으므로, 우리는 [데크]라고 읽는 것이 맞지 않을까 싶다.

지금까지 본 유사한 예로

height를 [헤이트]라고 읽은 k군,
enum을 [에넘]이라고 읽은 l씨,
등이 생각난다.

사전 한번만 찾아보면 되는 것을, 자기가 알고 있는게 맞다는 굳은 신념때문에 안 찾아보기 때문일지도 -_-a


Posted by uhm