2013년 5월 22일

kami2d 라이브러리 (멀티터치)

아래 글에서는 멀티터치및 OpenGL에 대해 간략히 소개하였는데, 이를 응용하여 만든  kami2dlib 라는 라이브러리를 소개드립니다.



* 테스트한 멀티 터치 모니터가 IR 방식이라 허접합니다. 양해해 주시길...

1. 기능
  • 오픈지엘 기반으로 H/W (GPU) 가속 렌더링 
  • 렌더러 선택가능 (OpenGL, GDI, GDI+, D2D 등 :현재는 OGL 만 적용. 다른건 귀찮아서 -_-)
  • 2차원 벡터 그리기 지원 (점, 선, 삼각형, 사각형, 원, 호, 타원, 베지어 곡선, 폴리곤 등)
  • 알파 블랜딩 지원
  • 펜 및 선 스타일 변경 지원 (선 두께, 대쉬등의 스타일)
  • 줌, 팬, 회전 등의 이벤트 처리 지원 (마우스및 터치 인터페이스)
  • 오픈지엘 리스트 버퍼지원으로 고속 렌더링 가능
  • 행렬 입력 지원으로 다양한 선형변환 가능
  • 멀티 터치 시연 (데모 프로그램)
  • 활용 분야 : 2차원 데이타 고속 뷰어, 에디터 개발 등
2. 환경
  • Win32 지원 
  • DLL 등의 라이브러리로 개발
  • C++ 빌더/ M$ VS 환경 
    • C++ 빌더용 데모 프로그램에 멀티터치 기능을 연결해 놓음

3. 사용방법

사용방법은 View2d 를 상속받아 화면에 실제 그리는 부분(onRender) 함수를 구현해 주면 됩니다.


testview.h 

#ifndef testviewH
#define testviewH

#include <windows.h>
#include "kami2dlib.h"

class TestView : public View2d
{

public:
    virtual DWORD onRender();

private:
Painter2d* _pPainter;

public:
TestView()
     : View2d(_pPainter = new Opengl2dPainter)
    {}

    virtual ~TestView()
    {
    delete _pPainter;
        _pPainter = NULL;
    }
};


#endif




Painter 를 직접 생성하여 지정하는 이유는 (new Opengl2dPainter), 추후에 OpenGL 대신 다른 렌더링 라이브러리(Direct2D, GDI, GDI+) 를 사용할 경우에도 지원가능하도록 하기 위해 설계되었습니다.


testview.cpp

#include "testview.h"

DWORD TestView::onRender()
{
DWORD dwTickCount = ::GetTickCount();

Pen pen;
    pen.setColor(30, 30, 30);
    _pPainter->rotate( _pPainter->getRotate());
_pPainter->renderBegin(pen);

    // draw grids
    {
        Pen pen;
        pen.setColor(50, 50, 50);

        //10mm
        _pPainter->drawBegin(pen);
        for (float i = -200; i <= 200; i+=10)
            _pPainter->drawLine( i, 200, i, -200);
        _pPainter->drawEnd();

        _pPainter->drawBegin(pen);
        for (float i = -200; i <= 200; i+=10)
            _pPainter->drawLine( -200, i, 200, i);
        _pPainter->drawEnd();

    }

    // draw axis
    {
        Pen pen;
        pen.setColor(255, 0, 0);

        _pPainter->drawBegin(pen);
        _pPainter->drawLine( -200,0, 200,0);
        _pPainter->drawEnd();

        pen.setColor(0, 255, 0);
        _pPainter->drawBegin(pen);
        _pPainter->drawLine( 0, 200, 0, -200);
        _pPainter->drawEnd();
    }

    // draw entities
    {

    for(int i=0; i< 11; ++i)
        {
        for(int j=0; j< 11; ++j)
            {
            Matrix3 m;
                m.Translate( -100 +20*j, 100 - i*20);

                Pen pen(255, 255, 255);
                _pPainter->drawBegin(pen, m);
                _pPainter->drawRectangle( -7, 7, 7, -7);
                _pPainter->drawRectangle( -5, 5, 5, -5, true);
                _pPainter->drawEnd();
            }
        }
    }

_pPainter->renderEnd();

    return ::GetTickCount() - dwTickCount;
}

onRender 함수는 화면에 새로 그릴일이 있을때마다 callback 되는 함수로, 리턴값은 렌더링에 걸린 시간을 되돌려주면 됩니다. 
함수의 시작은 renderbegin , 끝은 renderend 해주시면 되며, 각각의 엔티티(점, 선, 사각형등의 figure)를 그릴때는 시작부분에 drawbegin, 끝에 drawend를 호출하여 사용가능합니다.

4. 오픈소스 프로젝트


kami2d 라이브러리는 GitHub 를 통해 오픈소스로 공개해 놓았습니다. 

2013년 5월 15일

C++ 빌더에서 오픈지엘 사용하기



glskeleton.h

//--------------------------------------------------------------------------- 
#include <vclvcl.h> 

//--------------------------------------------------------------------------- 
#ifndef GLSkeletonH 
#define GLSkeletonH 
//--------------------------------------------------------------------------- 
#include <vclClasses.hpp> 
#include <vclControls.hpp> 
#include <vclStdCtrls.hpp> 
#include <vclForms.hpp> 
#include <gl/gl.h> 
#include <gl/glu.h> 
//--------------------------------------------------------------------------- 
class TForm1 : public TForm 
{ 
published:    // IDE-managed Components 
    void __fastcall FormCreate(TObject *Sender); 
    void __fastcall FormDestroy(TObject *Sender); 
    void __fastcall FormResize(TObject *Sender); 
    void __fastcall FormPaint(TObject *Sender); 
private:    // User declarations 
    HDC hdc;
    HGLRC hrc; 
    int PixelFormat; 
public:        // User declarations 
    fastcall TForm1(TComponent* Owner); 
    void __fastcall IdleLoop(TObject*, bool&); 
    void __fastcall RenderGLScene(); 
    void __fastcall SetPixelFormatDescriptor(); 
}; 

glskeleton.cpp
//--------------------------------------------------------------------------- 
extern TForm1 *Form1; 
//--------------------------------------------------------------------------- 
#endif 

//--------------------------------------------------------------------------- 
#include <vclvcl.h> 
#pragma hdrstop 

#include "GLSkeleton.h" 
//--------------------------------------------------------------------------- 
#pragma resource "*.dfm" 

TForm1 *Form1; 
//--------------------------------------------------------------------------- 
__fastcall TForm1::TForm1(TComponent* Owner) 
    : TForm(Owner) 
{ 
    Application->OnIdle = IdleLoop; 
    _control87(MCW_EM, MCW_EM);
} 
//--------------------------------------------------------------------------- 
void __fastcall TForm1::IdleLoop(TObject*, bool& done) 
{ 
     done = false; 
     RenderGLScene(); 
     SwapBuffers(hdc); 
} 
//--------------------------------------------------------------------------- 
void __fastcall TForm1::RenderGLScene() 
{ 
     //Place your OpenGL drawing code here
} 
//--------------------------------------------------------------------------- 
void __fastcall TForm1::FormCreate(TObject *Sender) 
{ 
    hdc = GetDC(Handle); 
    SetPixelFormatDescriptor(); 
    hrc = wglCreateContext(hdc); 
    wglMakeCurrent(hdc, hrc); 
    SetupRC(); 
} 
//--------------------------------------------------------------------------- 
void __fastcall TForm1::SetupRC() 
{ 
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f); 
    glClear(GL_COLOR_BUFFER_BIT); 
    glFlush(); 
} 
//--------------------------------------------------------------------------- 
void __fastcall TForm1::FormDestroy(TObject *Sender) 
{ 
    ReleaseDC(hdc);
    wglMakeCurrent(hdc, NULL); 
    wglDeleteContext(hrc); 
} 
//--------------------------------------------------------------------------- 
void __fastcall TForm1::SetPixelFormatDescriptor() 
{ 
    PIXELFORMATDESCRIPTOR pfd = { 
        sizeof(PIXELFORMATDESCRIPTOR), 
        1, 
        PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, 
        PFD_TYPE_RGBA, 
        24, 
        0,0,0,0,0,0, 
        0,0, 
        0,0,0,0,0, 
        32, 
        0, 
        0, 
        PFD_MAIN_PLANE, 
        0, 
        0,0,0 
        }; 
    PixelFormat = ChoosePixelFormat(hdc, &pfd); 
    SetPixelFormat(hdc, PixelFormat, &pfd); 
} 
//--------------------------------------------------------------------------- 
void __fastcall TForm1::FormResize(TObject *Sender) 
{ 
    GLfloat nRange = 200.0f; 
    glViewport(0, 0, ClientWidth, ClientHeight); 
    glMatrixMode(GL_PROJECTION); 
    glLoadIdentity(); 
 
    
    if (ClientWidth <= ClientHeight) 
       glOrtho(-nRange, nRange, -nRange*ClientHeight/ClientWidth, 
          nRange*ClientHeight/ClientWidth, -nRange, nRange); 
    else 
       glOrtho(-nRange*ClientWidth/ClientHeight, nRange*ClientWidth/ClientHeight, 
          -nRange, nRange, -nRange, nRange); 

    glMatrixMode(GL_MODELVIEW); 
    glLoadIdentity(); 
} 
//--------------------------------------------------------------------------- 



위 코드는 C++ 빌더의 VCL 을 사용하는 환경에서 OPENGL 을 사용할 경우 기본적인 뼈대가 되는 코드이다
  • 몇몇 환경에서는 부동소수점 예외가 발생하는데 이때는 _control87(MCW_EM, MCW_EM); 을 사용하여 연산과정중 opengl 내부에서 발생한 예외를 지긋이 disable 시키자. 

  • 뷰가 여러개 있을 경우에는 렌더링 타켓이 바뀌는 경우가 있기 때문에 항상 새로그리기 전에는 wglMakeCurrent(hdc, hrc); 을 사용하여 타켓을 변경하도록 하자. 

  • 위 예에서는 IdleLoop 즉 윈도우 메시지 처리할것이 없을때 화면을 주기적으로 갱신하고 있는데, 지속적인 렌더링이 필요없는 경우에는 OnPaint 즉 영역을 다시 그리는것이 필요할때만 그려도 무방하다. 

멀티터치 제스쳐



멀티터치를 사용하기 위해서는

  • C++ Builder 의 경우 2010 이상 버전(XE, XE2, XE3, XE4 등 포함)에서는 쉽게 사용이 가능합니다. TGestureManager 및 포준 Gesture 를 지원하고 이벤트 핸들러를 직접 구현하여 처리도 가능합니다.
  • M$ V$ 의 경우 WM_GESTURE 메시지 이벤트 핸들러를 구현해주면 역시 사용이 가능합니다. 
//MFC 에서는 아래와 같은 Gesture handlers 를 구현
virtual BOOL OnGestureZoom(CPoint ptCenter, long lDelta); 
virtual BOOL OnGesturePan(CPoint ptFrom, CPoint ptTo);
virtual BOOL OnGestureRotate(CPoint ptCenter, double dblAngle); 
virtual BOOL OnGesturePressAndTap(CPoint ptFirstFinger, long lDelta); 
virtual BOOL OnGestureTwoFingerTap(CPoint ptCenter);

문자열을 토큰으로 분리하는 strtok

C/C++ 기반에서 문자열을 구분자로 사용해 각각의 토큰(token)으로 분리하는 방법들은 상당히 많이 있습니다. 그중 가장 오래된(?) 함수가 바로 strtok() 일것이다?

하지만 이 함수는 치명적인 문제가 있으니 바로 멀티쓰레드를 사용할 경우에 발생합니다.
문자열을 토큰으로 분리해주는 strtok 함수의 실제 구현내용을 살펴보면 ...

http://research.microsoft.com/en-us/um/redmond/projects/invisible/src/crt/strtok.c.htm


/* Copyright (c) Microsoft Corporation. All rights reserved. */

#include 

/* ISO/IEC 9899 7.11.5.8 strtok. DEPRECATED.
 * Split string into tokens, and return one at a time while retaining state
 * internally.
 *
 * WARNING: Only one set of state is held and this means that the
 * WARNING: function is not thread-safe nor safe for multiple uses within
 * WARNING: one thread.
 *
 * NOTE: No library may call this function.
 */

char * __cdecl strtok(char *s1, const char *delimit)
{
    static char *lastToken = NULL; /* UNSAFE SHARED STATE! */
    char *tmp;

    /* Skip leading delimiters if new string. */
    if ( s1 == NULL ) {
        s1 = lastToken;
        if (s1 == NULL)         /* End of story? */
            return NULL;
    } else {
        s1 += strspn(s1, delimit);
    }

    /* Find end of segment */
    tmp = strpbrk(s1, delimit);
    if (tmp) {
        /* Found another delimiter, split string and save state. */
        *tmp = '\0';
        lastToken = tmp + 1;
    } else {
        /* Last segment, remember that. */
        lastToken = NULL;
    }

    return s1;
}


위 구현내용을 보면 알겠지만 파싱과정에서 마지막 토큰을 가리키는 포인터(static char *lastToken ) 가 전역변수임을 알 수 있다.

결국 개별 쓰레드들이 strtok 함수를 사용하게 되면 지옥문이 열리는 것이다.

C언어의 오래된 몇몇 함수들은 멀티쓰레드 개념이 없던 때 작성된것이라 strtok 와 같은 쓰레드 안전하지 않은 함수들이 다수 있다는 사실. 

M$에서는 이런 문제를 해결하고자 TLS(Thread Local Storage)라는 것을 발명해 냈고, 위 코드처럼 오래된 C 함수들의 문제를 예방하고자,  전역변수이지만 개별 쓰레드에서만 전역이 되도록 해주는 방식을 고안해 냈다. 참신하다고 해야하나?

코딩시에는 개발툴에서 DLL 링크 옵션중 "MultiThread DLL Debug" 와 같이 멀티쓰레드 인지 여부를 선택하는것만으로 알아서(?) 해결해주고는 있다.

어쨋든 문자열 토큰분리를 하는데 strtok 와 같이 오래된 놈을 사용하고 싶지 않다면, 아래와 같은 것도 괜찮을것 같다.

http://stackoverflow.com/questions/289347/using-strtok-with-a-stdstring


void tokenize(const std::string& str, const std::string& delim, std::vector< std::string>& tokens)
{
std::size_t start, end = 0;
while (end < str.size())
{
start = end;
while (start < str.size() && (delim.find(str[start]) != std::string::npos))
{
start++;  // skip initial whitespace
}
end = start;
while (end < str.size() && (delim.find(str[end]) == std::string::npos))
{
end++; // skip to end of word
}
if (end-start != 0)
{
// just ignore zero-length strings.
tokens.push_back( std::string(str, start, end-start));
}
}
}

위 예는 원본 문자열을 주고 구분자를 전달하면 토큰으로 분리된 결과가 STL 벡터 컨테이너로 리턴되는 구조로 strtok 와는 구현 방식은 다르지만 멀티쓰레드와 관련된 귀찮은 걱정은 하지 않아도 되겠다.

시리우스 라이브러리 홈페이지 오픈

현재 시리우스(Sirius) 라이브러리라는 제품을 개발하고 이를 소개하는 홈페이지를 오픈 하였습니다. 관심있는 분들의 많은 방문 요청드립니다. 앞으로 업데이트 소식및 변경사항은 스파이럴랩 홈페이지를 통해 진행할 예정입니다. 스파이럴랩 홈페이지 :  h...