마우스를 이용한 View 회전

예전에 나이스가이님이 질문한 마우스를 이용해 현재 화면을 회전하면서 화면상의 물체를 살펴보는 방법에 대해 간단하게 설명해 보려한다. 전문용어(?)로는 Arcball 기법이라고 하는데, 이 Arcball을 구현하는 방법으로 직접 회전행렬을 계산해서 OpenGL의 glMultMatrix를 이용해 적용하는 방법 하나와 x와 y축에 대한 회전각도만을 계산해서 glRotatef를 이용해 적용하는 방법이 있다. 첫번째 방법은 NeHe의 강좌에 소개되어져있으나 URL은 http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=48 이다. 하지만 필자는 두번째 방법을 이용해 Arcball를 구현해 보려고 한다. 이유인 즉, 첫번째 방법은 OpenGL에서는 기본적으로 행렬 Type와 행렬간의 연산자 함수를 제공해주지 않으므로, 회전행렬을 직접 구하기 위한 행렬 Type을 개발자가 직접 정의해줘야 하고 행렬연산을 만들어줘야하는 번거로움이 있는 반면, 두번째 방법은 단지 x와 y축에 대한 회전각도 값을 실수형으로도 충분하기 때문이다.

글자만 있으면 썰렁하므로 별 의미도 없을법한 최종 결과 스크린샷은 아래와 같다.


화면상에 OpenGL의 GLUquadricObj을 이용하여 실린더를 6개를 그렸으며, 마우스를 이용하여 실린더 6개를 회전시키면서 살펴볼 수 있다. 보다 자세한 조작법은 마우스 오른쪽 버튼을 누른 상태에서 좌우로 마우스를 이동하면 Y축으로 회전되며 상하로 마우스를 이동하면 X축으로 회전한다. 이러한 사용자 액션과 반응을 염두해 두고 코드를 살펴보면 이해하는데 큰 도움이 될 것이다.

먼저 마우스 조작에 따른 Arcball 기법을 적용하기 이전에 화면상에 6개의 실린더를 그려주는 코드는 다음과 같다.

int DrawGLScene()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();
	glTranslatef(0.0f, 0.0f, -20.0f);

	bool bToggle = true;
	for(float offset=-5; offset<=5; offset+=2) {
		if(bToggle) glColor3f(0.6f, 0.6f, 1.0f);
		else glColor3f(1.0f, 0.6f, 0.6f);
		bToggle = !bToggle;

		glPushMatrix();
		glTranslatef(offset, 0.0f, 0.0f);
		glTranslatef(0.0f, 0.0f, -8.0f);
		gluCylinder(obj, 1.0f, 1.0f, 16.0f, 38, 4);
		glPopMatrix();
	}

	return TRUE;			
}

코드를 보고 이미 짐작하고 계실 분도 있겠지만, 이 코드는 NeHe의 강좌에서 제공하는 코드를 기본으로 하고 있다. 혹시 필요한 분들을 위해 전체코드를 다운로드 할 수 있도록 하겠다.

이제 위의 코드에서 마우스를 이용해 상하좌우로 회전시키면서 6개의 실린더를 구석구석 관찰하는 코드를 추가해보자.

먼저 마우스의 Drag를 통해 계산될 X축과 Y축에 대한 회전값을 위한 변수는 다음과 같다.

GLfloat xAngle;
GLfloat yAngle;
POINT mouseDownPt;

언급하지 않은 mouseDownPt 변수가 있는데, 이것은 마우스 버튼을 누른 좌표 지점을 저정하기 위한 변수이다. 마우스 버튼을 누른 지점과 마우스 버튼을 누른 상태에서 마우스를 이동하였을때 X축과 Y축으로 얼마만큼 이동되었는지를 계산하고 이 계산된 값을 이용해 xAngle와 yAngle값이 계산된다.

일단 xAngle와 yAngle가 계산되었다고 가정하고 최종적으로 회전을 적용시킨 코드는 가장 앞서 언급한 코드인 DrawGLScene 함수의 수정에 있다.

int DrawGLScene()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();							
	glTranslatef(0.0f, 0.0f, -20.0f);

	glRotatef(xAngle, 1.0f,0.0f,0.0f);
	glRotatef(yAngle, 0.0f,1.0f,0.0f);

	bool bToggle = true;
	for(float offset=-5; offset<=5; offset+=2) {
		.
		.
		.
	}

	return TRUE;			
}

새롭게 추가된 오렌지색 두줄의 코드가 전부이다. X축과 Y축의 회전에 맞게 glRotatef의 함수의 인자가 사용되었음을 주의해서 보기 바란다.

이제 마지막으로 마우스의 액션에 따른 실제 xAngle와 yAngle의 값을 계산하는 방법을 알아보도록하자. 이 계산 코드는 마우스 액션에 대한 Event에 위치하게 되는데, MouseDown, MouseMove, MouseUp 이벤트에 그 코드가 존재한다.

	case WM_LBUTTONDOWN:
	{
		mouseDownPt.x = GET_X_LPARAM(lParam);
		mouseDownPt.y = GET_Y_LPARAM(lParam);

		SetCapture(hWnd);

		return 0;
	}

위의 코드는 간단히 앞에서 정의한 mouseDownPt 변수에 가장 처음 마우스 버튼이 눌려진 위치를 저장하고 있으며 SetCapture  API를 호출해서 마우스가 Window를 벗어나도 지속적으로 Mouse Message를 받을수 있도록 하고 있다.

	case WM_MOUSEMOVE:
	{
		if(GetCapture() == hWnd) {
			int X = GET_X_LPARAM(lParam);
			int Y = GET_Y_LPARAM(lParam);

			xAngle += (Y-mouseDownPt.y) / 3.6;
			yAngle += (X-mouseDownPt.x) / 3.6;

	                InvalidateRect(hWnd, NULL, FALSE);

			mouseDownPt.x = X;
			mouseDownPt.y = Y;
		}

		return 0;
	}

위의 코드는 마우스를 이동했을때 호출되는 코드인데, GetCapture를 이용해서 현재 마우스 버튼이 눌러졌는지를 검사하고, 만약 마우스가 눌려진 상태에서 움직였다면 바로 여기서 xAngle와 yAngle를 계산한다. 계산공식을 눈여겨 보길 바란다. 3.6으로 나눠주고 있는데, 이것은 마우스를 1 픽셀 이동했을 경우 1/3.6 회전, 즉 0.27777778도 회전하게 되는데, 이 회전량이 사용자가 가장 자연스럽게 느껴지므로 3.6으로 나눴다. 좀더 회전정도를 강하게 하고 싶다면 이 값을 줄이면 회전이 팍팍!! 되도록 할 수 있다.

	case WM_LBUTTONUP:
	{
		ReleaseCapture();
		return 0;
	}

끝으로 위의 코드는 마우스 버튼을 눌러 이동하면서 물체를 이리저러 살펴보다가 마우스 버튼을 누름을 해제시킬때 발생하는 코드로써, ReleaseCapture API를 호출한다. ReleaseCapture는 SetCapture를 호출한 윈도우가 반드시 호출해줘야 하는 Couple API로써 SetCapture에 의한 마우스 메세지의 독점처리를 해제하는 함수이다.

이상으로 간단하게 마우스를 이용한 Arcball 기법에 대해 살펴보는 것을 마치도록 하며, 아래에 해당 소스를 링크한다.

너 “또라이”지?

오늘 문제의 원인을 알고 내가 나한테 던진 말이다.

“너.. 또라이지?” ㅋㅋ

문제는 MySQL에서 BLOB Type으로 데이터를 저장한 후에 읽어보니 전체 데이터가 아닌 일부만 읽혀지는 것이였다. 테이블에 저장한 그림파일의 크기는 195,881KB. 그런데 읽혀지는 데이터 길이는 65,535KB. 저장할때 잘못된 건지.. 읽어올때 잘못된 것인지 고민하다가 이것저것.. MySQL 환경설정 파일도 건드려 보고 말이다. 환경설정 파일을 보고 MySQL… 이거 제대로 쓸라면 공부할거 너무 많은것 같다.. 라는 부담스런 생각도 들고… 뭐 여튼… 아무리 인터넷을 찾아봐도 BLOB 데이터가 전부 오지 않는 이유는 찾을 수가 없었다. 구글신에게 도움을 요청해도 없다는건…. 뭔가 이유가 “얼토당토” 않은 것이라는 확율이 90%이상이란 말인데… 하지만 여전이 나는 “MySQL 이놈이 분명이 BLOB 데이터의 크기를 제한하고 있는게 분명해. 봐… 65,535KB만 보내주잔아? 즉, 64K만 보내주고 있다구.. 분명 환경설정에서 전송 크기를 늘려주는 환경변수가 있을 것이고 그것만 늘려주면 될것야..”라고 확신하며 그다지 흥미롭지 않은 MySQL 환경 변수만을 뒤지기 시작했으나.. 도대체 이놈이다 싶은 놈이 없었다. 음… 혹시 BLOB의 최대 크기가 64KB아냐? 라는 생각에 찾아보니 그냥 BLOB만 있는게 아니라 TINYBLOB, MEDIUMBLOB, LONGBLOB 라는 Type도 있는게 아닌가.. 각각의 최대 크기를 보니까

  • BLOB는 2^16-1KB
  • TINYBLOB는 2^8-1KB
  • MEDIUMBLOB는 2^24-1KB
  • LONGBLOB는 2^32-1KB

였다. 봐.. 다 “65,535KB보다 크잔아..”라고 자신있게 확신한 후에, 역시 원인은 MySQL의 환경변수에 있어 하고 있는데..  근데 2^16이 몇이지? 라는 생각에 계산기를 뚜드려보니.. 2^16 = 65536 !! 으아~~ ㅜ_ㅜ 저 또라이 맞죠? ㅠ_ㅜ 그데 쓰고보니 또라이는 좀 심했다는 생각에 또 다시 ㅠ_ㅠ

나 이제 퇴근할래.. 지하철 끊기기 전에~

[C++] LPCSTR Type을 LPOLESTR Type으로 변환

HRESULT __fastcall AnsiToUnicode(LPCSTR pszA, LPOLESTR* ppszW) { 
    ULONG cCharacters;
    DWORD dwError;

    // If input is null then just return the same.
    if (NULL == pszA)
    {
        *ppszW = NULL;
        return NOERROR;
    }

    // Determine number of wide characters to be allocated for the
    // Unicode string.
    cCharacters =  strlen(pszA)+1;

    // Use of the OLE allocator is required if the resultant Unicode
    // string will be passed to another COM component and if that
    // component will free it. Otherwise you can use your own allocator.
    *ppszW = (LPOLESTR) CoTaskMemAlloc(cCharacters*2);
    if (NULL == *ppszW)
        return E_OUTOFMEMORY;

    // Covert to Unicode.
    if (0 == MultiByteToWideChar(CP_ACP, 0, pszA, cCharacters,
                  *ppszW, cCharacters))
    {
        dwError = GetLastError();
        CoTaskMemFree(*ppszW);
        *ppszW = NULL;
        return HRESULT_FROM_WIN32(dwError);
    }

    return NOERROR;
}

SOAP, SOAP, SOAP, SOAP.. 지금 내 머리속엔 비누방울이 아른 아른~ 아침 출근할때 감기 바이러스가 몸에 침투를 했나부다. 몸에 열… 점심때 후식으로 먹은 빵이 불량식품이였나부다. 속이 부글부글.. 뼈를 싹인다는 Coco Cola로 일단 속을 달래는 중….

으.. 아까 콜라 사러 1층 편의점에 가려고 엘리베이터타려고 기다리는데 (사무실은 11층).. 이 놈의 건물의 엘리베이터의 움직이는 속도가 정말 장난이 아니다.. 가끔 엘리베이터를 기다리는 건지… 한없이 감감 무소식으로 오지 않는 지하철을 기다리는건지….. 성질 급한 사람은 가끔 화가 날 법도 하다.. 나같이 오늘처럼 말이다..

Template을 이용한 Observer 패턴 – 2단계

이제 여기서부터는 개선의 여지를 찾아보겠습니다. 어떻게 하면 EventSrc 클래스가 담당하고 있는 Fire 함수를 Observed에게 전가시킬 수 있을까요? 그래서 EventSrc 클래스를 없앨 수 있겠는가? 그 해답은 함수자(Functor)에 있습니다.

먼저 연습겸해서 간단하게 함수자를 사용해서 1단계의 코드를 수정해 보도록 하겠습니다. 아래는 새롭게 추가한 함수자에 대한 클래스입니다.

class Functor {
private:
    int arg_;

public:
    Functor(int arg) {
        arg_ = arg;
    }

    void operator()(Observable *pOb) const {
        pOb->OnEvent(arg_);
    }
};

이 함수자를 사용하면, EventSrc 클래스의 Fire 함수가 아래처럼 무척 깔끔하게 작성됩니다.

void Fire(int a)
{
    std::for_each(m_listObserver.begin(), m_listObserver.end(), Functor(a));
}

하지만 이런 방식은 Fire 함수를 깔끔하게 만들어준다는 사소한 장점은 있지만, 다른 어플리케이션에 적용할때 매번 함수자를 각각의 경우에 맞게 새롭게 코딩해줘야 하는 수고로움이 더욱 많습니다. 또한 여전이 Observed에서는 자신이 관리하고 있는 Observer 객체들이 호출해야할 함수가 무엇인지를 알 길이 없습니다.

하지만 여기서 곰곰이 생각해 보면 이 함수자를 Observed 클래스의 inner class로 정의해보는 것에 대한 아이디어가 떠오릅니다. 게다가 이 함수자 역시 template로 정의해서 Observed 클래스가 관리하고 있는 Observer 객체들이 호출해야할 함수를 타입으로 받아 버린다면 Observed가 알아야할 Observer의 정보를 모두 Observed에게 넘겨줄 수 있게되어, Fire의 책임을 Observed가 맡을 수 있게 됩니다. 그러면 더 이상 EventSrc는 필요치 않게 되고요. 이러한 아이디어에 착안해서 새롭게 구현된 Observed 클래스는 다음과 같습니다.

template 
class Observed
{
public:
    class Firer1 {
    public:
        explicit Firer1(T_result (T::*pMember)(T_arg1), T_arg1 arg1) {
            m_pMemFunc = pMember;
            m_arg1 = arg1;
        }

        T_result operator()(T* pClass) const {
            return ((pClass->*m_pMemFunc)(m_arg1));
        }

    private:
        T_result (T::*m_pMemFunc)(T_arg1);
        T_arg1 m_arg1;
    };

public: // type define
    typedef std::list typeObservers;

protected: // private attribute
    typeObservers m_listObserver;
    T_result (T::*m_pMemFunc)(T_arg1);
}

public: // ctr & dtr
    Observed(T_result (T::*pMember)(T_arg1)) {
        m_pMemFunc = pMember;
    }
    virtual ~Observed() {}

public: // operator
    void RegisterObserver(T *pOb)
    {
        m_listObserver.push_back( pOb );
    }

    void UnRegisterObserver(T *pOb)
    {
        m_listObserver.remove(pOb);
    }

public: // Fire!
    void Fire(T_arg1 arg1)
    {
        std::for_each(m_listObserver.begin(), m_listObserver.end(), 
            Firer1(m_pMemFunc, arg1));
    }
};

굵은 청색 폰트로 된 것이 수정되거나 새롭게 추가된 것입니다. Observed가 관리하고 있는 Observer의 호출함수의 반환값과 인자, 그리고 호출함수에 대한 타입을 template 인자인 T_result와 T_arg1으로 받고, 호출함수는 생성자에서 받아 맴버변수인 m_pMemFunc에 저장하도록 하였습니다.

이렇게 새롭게 구현된 Observed를 이용해서 Client 측에서 사용하는 코드는 다음과 같습니다.

int _tmain(int argc, _TCHAR* argv[])
{
    Observed *pES = 
        new Observed(&Observable::OnEvent);

    Observable_A *pOA = new Observable_A();
    Observable_B *pOB = new Observable_B();

    pES->RegisterObserver(pOA);
    pES->RegisterObserver(pOB);

    pES->Fire(99);

    delete pOA;
    delete pOB;
    delete pES;

    return 0;
}

이제 마지막으로 하나 더 짚고 정리를 하겠습니다. 여기서 한가지 큰 문제가 있는데 그것은 Observed가 관리하고 있는 Observer의 호출해야할 함수의 인자 개수에 관한 문제입니다. 지금가지의 경우는 단지 하나의 인자만을 받는 경우지만 두개 이상의 인자를 받는 경우에 대한 처리도 필요하지요. 하지만 이것 역시 그리 어렵지 않게 해결할 수 있습니다. Observed의 inner class인 함수자의 이름이 Firer1인 이유는 하나의 인자를 받는 함수자이기 때문에 Firer 뒤에 1을 붙인 것입니다. 그렇다면 이제 2개의 인자를 받는 Firer2 함수자를 정의하고 인자를 2개를 받는 Fire 맴버함수를 하나더 만들어 두면 됩니다.

이제 Client는 1단계처럼 관리하고자 하는 Observer에 대해 EventSrc와 Observable 클래스 모두를 정의할 필요가 없이, Observable 클래스 단하나만 신경 쓰면 되게 되었습니다. 그리고 Observer에 대한 모든 관리에 대한 책임을 오직 Observed가 맡게 되어 SRP를 지키게 되었습니다.