2014년 7월 23일

SDL2 기반의 OPENGL 렌더링 데모 2부

앞선 글에서는 간단한 도형만을 보여주였지만 이번 예제에서는 외부에서 모델 파일을 가져와 좀더 복잡한 형상을 렌더링 해 보자.

요새 각광받은 3D프린터 들을 보면 수많은 모델들이 STL (STereoLithography) 이란 파일 포맷으로 제공되고 있다. 이번 데모에서는 이 파일 포맷을 렌더링 해보자. STL 파일 포맷에 대한 정보는  http://en.wikipedia.org/wiki/STL_(file_format) , http://www.eng.nus.edu.sg/LCEL/RP/u21/wwwroot/stl_library.htm#What 에서 참고 바라며, 이번 예제에서는 바이너리 기반의 STL 파일포맷을 대상으로 한다.

이 데모의 실제 동작 화면을 캡춰해 http://www.youtube.com/watch?v=LTGfNCY0wIA 에 올려놓았다.

자 그렇다면 실제 구현에 들어가자. 마찬가지로 App 를 상속받는다.

--------------------------ex3.h
#ifndef EX3_H
#define EX3_H
 
#include "app.h"
#include <vector>
 
class Example3 : public App
{
public:
 bool OnInit(int argc, TCHAR* argv[]);
 void OnEvent(SDL_Event* Event);
 void OnLoop();
 void OnRender();
 void OnCleanUp();
 
private:
 bool LoadSTL(TCHAR* lpszFilename);
 
#pragma pack(push, 4)
 typedef struct
 {
  float x;
  float y;
  float z;
 }VERTEX; 
 typedef struct
 {
  VERTEX normal;
  VERTEX v[3];
 }FACE;
#pragma pack(pop)
 
 std::vector<face> _faces;
 VERTEX _center;
 GLuint _listid;
 float _scale;
 
public:
 Example3();
 virtual ~Example3();
};
 
#endif
-----------------------------------------ex.cpp

#include "ex3.h"
#include "log.h"

// STL 예제 파일들은 인터넷에서 검색하여 구하도록 한다
TCHAR* gszSamples[] = {
 _T("samples\\Chest_Back.stl"),
 _T("samples\\knot.stl"),
 _T("samples\\porsche.stl"),
 _T("samples\\pump.stl"),
 _T("samples\\tire_v.stl")
};

Example3::Example3()
{
 _scale = 1.0f;
}

Example3::~Example3()
{
}

bool Example3::LoadSTL(TCHAR* lpszFilename)
{
 FILE* fp = _tfopen( lpszFilename, _T("rb"));  //lpszFilename
 if (!fp)
 {
  Log("fail to open the stl file : %s", lpszFilename);
  return false;
 }
 _faces.clear();
 glDeleteLists(_listid, 1);

 // STL 이진 파일 포맷에 맞게 얻어온다
 BYTE header[80];
 fread(header, sizeof(header), 1, fp);
 UINT32 numOfFacets=0;
 fread(&numOfFacets, sizeof(numOfFacets), 1, fp);
 Log("face count = %d", numOfFacets);
 
 FACE f;
 UINT16 attr;
 for (int i=0; i< numOfFacets; i++)
 {
  fread(&f.normal, sizeof(f.normal), 1, fp);
  fread(&f.v, sizeof(f.v), 1, fp); 
  fread(&attr, 2, 1, fp);
  _faces.push_back(f);
 }
 fclose(fp);
 float xmin, xmax;
 float ymin, ymax;
 float zmin, zmax;
 xmin = 9999; xmax = -9999;
 ymin = 9999; ymax = -9999;
 zmin = 9999; zmax = -9999;

 // 크기를 알아내에 화면 중심으로 이동시키기 위해서
 for (int i=0; i< _faces.size(); i++)
 {
  if (_faces[i].v[0].x  < xmin)
   xmin = _faces[i].v[0].x;
  if (_faces[i].v[0].x  > xmax)
   xmax = _faces[i].v[0].x;
  if (_faces[i].v[0].y  < ymin)
   ymin = _faces[i].v[0].y;
  if (_faces[i].v[0].y  > ymax)
   ymax = _faces[i].v[0].y;
  if (_faces[i].v[0].z  < zmin)
   zmin = _faces[i].v[0].z;
  if (_faces[i].v[0].z  > zmax)
   zmax = _faces[i].v[0].z;
 }
 _center.x = (xmin+xmax) / 2.0f;
 _center.y = (ymin+ymax) / 2.0f;
 _center.z = (zmin+zmax) / 2.0f;
 
 // 렌더링 속도 향상을 위해 리스트 버퍼 사용
 _listid = glGenLists( 1 );
 glNewList(_listid, GL_COMPILE );
 glBegin( GL_TRIANGLES );
 glColor3f( 0.3, 0.8, 0.1);
 for (int i=0; i< _faces.size(); i++)
 {
  glNormal3fv( (float*)&_faces[i].normal);
  glVertex3fv( (float*)&_faces[i].v[0]);
  glVertex3fv( (float*)&_faces[i].v[1]);
  glVertex3fv( (float*)&_faces[i].v[2]);
 }
 glEnd();
 glEndList();
 return true;
}

bool Example3::OnInit(int argc, TCHAR* argv[])
{

 // 조명 효과 사용
 float ambLight[] = {0.7f, 0.7f, 0.7f, 1.0f};
 float specular[] = {1.0f, 1.0f, 1.0f, 1.0f};
 float specref[] = {1.0f, 1.0f, 1.0f, 1.0f};
 float lightPos[] = {500, 500, 300, 1.0f};
 glEnable(GL_LIGHTING);
 glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambLight);
 glLightfv(GL_LIGHT0, GL_DIFFUSE, ambLight);
 glLightfv(GL_LIGHT0, GL_SPECULAR, specular);
 glLightfv(GL_LIGHT0, GL_POSITION, lightPos);
 glEnable(GL_COLOR_MATERIAL);
 glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);
 glMaterialfv(GL_FRONT, GL_SPECULAR, specref);
 glMateriali(GL_FRONT, GL_SHININESS, 128);
 glEnable(GL_LIGHT0);
 return true;
}

void Example3::OnEvent(SDL_Event* e)
{
  // q : 줌 인
  // z : 줌 아웃
  // p : 렌더링 모드 변경
  switch( e->type )
  {
  case SDL_KEYDOWN:
    if (e->key.keysym.sym == 'q')
    {
     _scale *= 1.2;
    }
    else if (e->key.keysym.sym == 'z')
    {
     _scale *= 0.8;
    }
    break;
        case SDL_KEYUP:
   if (e->key.keysym.sym == 'p')
   {
    static int mode = 0;
    glPolygonMode( GL_FRONT_AND_BACK, GL_POINT+ mode );
    mode++;
    mode= mode % 3;
   }
   else if (e->key.keysym.sym >= '1' && e->key.keysym.sym <= '5')
   {
    int n = e->key.keysym.sym - '1';
    this->LoadSTL( gszSamples[n] );
   }
   
    break;
  default:
      break;
  }
}

void Example3::OnLoop()
{}

void Example3::OnRender()
{
 glClearColor( 0.1f, 0.1f, 0.1f, 1.0f );
 glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
 glLoadIdentity();
 gluLookAt(300, 300, 150, 0,0,0, 0,0,1);

 static float angle = 0;
 glRotatef( angle +=0.5, 0,0,1);

 glPushMatrix();
  glScalef(_scale, _scale, _scale);
  glTranslatef(-_center.x, -_center.y, -_center.z);
  glCallList(_listid);
 glPopMatrix();

 glBegin(GL_LINES);
  glColor3f(1, 0, 0);
  glVertex3f( 0, 0, 0 );
  glVertex3f( 500, 0, 0 );
  glColor3f(0, 1, 0);
  glVertex3f( 0, 0, 0 );
  glVertex3f( 0, 500, 0 );
  glColor3f(0, 0, 1);
  glVertex3f( 0, 0, 0 );
  glVertex3f( 0, 0, 500 );
 glEnd();
}

void Example3::OnCleanUp()
{
}

---------------------------------------------------

자 그럼 메인 함수를 보자

#include "app.h"
#include "ex3.h"
int _tmain(int argc, TCHAR* args[])
{ 
 App* app = new Example3;
 int n = app->Execute(argc, args);
 return n;  
}


각종 예제 모델 파일들(STL)은 http://www.eng.nus.edu.sg/LCEL/RP/u21/wwwroot/stl_library.htm#Download 에서 다운로드 받은 것들이다. 또한 키보드 1~5번 키를 누를때 마다 모델파일을 새로 로드하도록 구현되었다.

SDL2 기반의 OPENGL 렌더링 데모 1부

앞선글에서 SDL2 및 이를 쉽게 OPENGL 기반으로 응용할수 있는 개발 환경을 소개한 바 있다. App 라는 클래스를 상속받아 렌더링 부분등의 이벤트만 구현해주면 쉽게 접근이 가능하다.




위 이미지와 같이 3차원 공간에 몇개의 도형을 렌더링 하는 코드를 작성해 보도록 하자.
우선 할것은 App 를 상속받는 것이다.

---------------------------------------ex1.h

#ifndef EX1_H
#define EX1_H

#include "app.h"

class Example1 : public App
{
public:
 bool OnInit(int argc, TCHAR* argv[]);
 void OnEvent(SDL_Event* Event);
 void OnLoop();
 void OnRender();
 void OnCleanUp();

private:

public:
 Example1();
 virtual ~Example1();
};

#endif


다음으로 할것은 실제 이벤트 핸들러등을 구현하는 것이다.

--------------------------------------- ex1.cpp

#include "ex1.h"
#include "log.h"


Example1::Example1()
{}

Example1::~Example1()
{

}

bool Example1::OnInit(int argc, TCHAR* argv[])
{
 return true;
}

void Example1::OnEvent(SDL_Event* Event)
{}

void Example1::OnLoop()
{}

void Example1::OnRender()
{
    glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    glLoadIdentity();
 gluLookAt(5,5,10, 0,0,0,0,1,0);

 static float angle =0;
 glRotatef(angle += 1, 0,1,0);

    glBegin( GL_TRIANGLES );           
   glColor3f(1, 1, 1);
      glVertex3f(  0.0f,  1.0f, 0.0f );
      glVertex3f( -1.0f, -1.0f, 0.0f );
      glVertex3f(  1.0f, -1.0f, 0.0f );
    glEnd( );                         
    
 glPushMatrix();
    glTranslatef( 3.0f, 0.0f, 0.0f );

    glBegin( GL_QUADS );               
   glColor3f(1, 1, 0);
      glVertex3f( -1.0f,  1.0f, 0.0f );
      glVertex3f(  1.0f,  1.0f, 0.0f );
      glVertex3f(  1.0f, -1.0f, 0.0f );
      glVertex3f( -1.0f, -1.0f, 0.0f );
    glEnd( );                         
    glPopMatrix();

 glBegin(GL_LINES);
   glColor4f(1, 0, 0, 1);
      glVertex3f( 0.0f, 0.0f, 0.0f );
      glVertex3f( 5.0f, 0.0f, 0.0f );

   glColor4f(0, 1, 0, 1);
      glVertex3f( 0.0f, 0.0f, 0.0f );
      glVertex3f( 0.0f, 5.0f, 0.0f );

   glColor4f(0, 0, 1, 1);
      glVertex3f( 0.0f, 0.0f, 0.0f );
      glVertex3f( 0.0f, 0.0f, 5.0f );
 glEnd();

}

void Example1::OnCleanUp()
{

}



자 모든 구현이 완료되었다. 오직 화면을 렌더링하는 이벤트(OnRender)만 구현하였으며 다른 이벤트는 모두 미 구현상태이다. 특히 OpenGL 함수들로만 구성되어 있어 SDL 등의 익숙치 않는 함수들은 전혀 포함되어 있지 않아 쉽게 응용이 가능하게 되었다.

시작점 즉 메인함수의 모습은 아래와 같다.

#include <windows.h>
#include "app.h"
#include "ex1.h"

int _tmain(int argc, TCHAR* args[])
{ 
 App* app = new Example1;
 int n = app->Execute(argc, args);
 return n;  
}

다음 글에서는 좀더 복잡한 것들을 해보자

SDL2 기반의 OPENGL 테스트 프레임워크

SDL (Simple DirectMedia Layer) 라이브러리는 수많은 게임들에 적용된 유명한 오픈소스 프로젝트 인데, 그래픽 환경을 보면 2D 기반임을 알 수 있다.


이를 OpenGL로 변경하여 손쉽게 개발환경을 구축해 보자. 참고로 버전업된 SDL2 가 최신버전이므로 이를 기반으로 OpenGL 테스트용 환경을 만들어 보자.
기본 구조는 https://github.com/MetaCipher/sdl-2.0-basics 을 참고하였으며, 입맞에 맞게 대폭 수정하였다.

SDL 이 처음이신분들은 http://lazyfoo.net/tutorials/SDL/index.php 에서 다양한 예제를 제공하고 있으니 방문 바란다.

사용된 SDL2 버전은 2.0.3 이며 OpenGL2.1 및 1024*768 해상도에서 동작한다.


1. App.h
#ifndef APP_H
#define APP_H

/*******************************************************************************

  SDL2 Demo Application

        ----------------
        | App          |
        ----------------
        | Execute      |
        | OnInit       |
        | OnEvent      |
        | OnLoop       |
        | OnRender     |
        | OnCleanUp    |
        ----------------
            /|\
             |
             |
             |
         your inherited class

  email : sepwind@gmail.com (blog : http://sepwind.blogspot.com)
  base code from : https://github.com/MetaCipher/sdl-2.0-basics (Tim Jones @ SDLTutorials.com)
  and revisied by sepwind.

*******************************************************************************/

#include "SDL.h"
#include "SDL_opengl.h"
#include <gl\gl.h>
#include <gl\glu.h>
#include <stdio.h>
#include <tchar.h>

#pragma comment(lib, "sdl2.lib")
#pragma comment(lib, "opengl32.lib")
#pragma comment(lib, "glu32.lib")


class App
{
protected:
   int _windowWidth;
   int _windowHeight;
   bool _running;
   SDL_Window* _pWindow;
   SDL_GLContext _glcontext;

private:
   bool Init();
   bool InitGL();
   void Render();
   void CleanUp();

public:
   virtual bool OnInit(int argc, TCHAR* argv[])=0;
   virtual void OnEvent(SDL_Event* Event)=0;
   virtual void OnLoop()=0;
   virtual void OnRender()=0;
   virtual void OnCleanUp()=0;

public:
   App();
   virtual ~App();
   int Execute(int argc, TCHAR* argv[]);
   int GetWindowWidth();
   int GetWindowHeight();
};

#endif


2. App.cpp


#include "app.h"
#include "log.h"

App::App()
{
   _running = true;
   _pWindow = NULL;
   _glcontext = NULL; 
   _windowWidth = 1024;
   _windowHeight = 768;
}

App::~App()
{
}

int App::GetWindowWidth()  { return _windowWidth; }
int App::GetWindowHeight() { return _windowHeight; }

bool App::Init()
{
   if(SDL_Init(SDL_INIT_VIDEO) < 0)
   {
      Log("Unable to Init SDL: %s", SDL_GetError());
      return false;
   }

   if(!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"))
   {
      Log("Unable to Init hinting: %s", SDL_GetError());
   }

   if((_pWindow = SDL_CreateWindow(
       "SDL2 DEMO - sepwind@gmail.com (http://sepwind.blogspot.kr)",
       SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
       _windowWidth, _windowHeight, SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL)) == NULL)
   {
      Log("Unable to create SDL Window: %s", SDL_GetError());
      return false;
   }
 
   _glcontext = SDL_GL_CreateContext(_pWindow);
   SDL_GL_SetSwapInterval(1);

   this->InitGL();
   Log("success to initialized ... ");
   return true;
}

bool App::InitGL()
{
    glShadeModel( GL_SMOOTH );
    glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
    glClearDepth( 1.0f );
    glEnable( GL_DEPTH_TEST );
    glDepthFunc( GL_LEQUAL );
    glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );
  
    GLfloat ratio = ( GLfloat )_windowWidth / ( GLfloat )_windowHeight;
    glViewport( 0, 0, ( GLsizei )_windowWidth, ( GLsizei )_windowHeight );
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();
    gluPerspective( 45.0f, ratio, 0.1f, 1000.0f ); // z-near/far

    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
    return true;
}

void App::Render()
{
   SDL_GL_MakeCurrent(_pWindow, _glcontext);
   this->OnRender();
   SDL_GL_SwapWindow(_pWindow);
}

void App::CleanUp()
{
   Log("cleaning up ... ");
   this->OnCleanUp();
   if (_glcontext)
   {
      SDL_GL_DeleteContext(_glcontext);
      _glcontext = NULL;
   }
   if(_pWindow)
   {
      SDL_DestroyWindow(_pWindow);
      _pWindow = NULL;
   }
   SDL_Quit();
}

int App::Execute(int argc, TCHAR* argv[])
{
   if(!this->Init())
   {
      Log("fail to initialize SDL2");
      return -1;
   }

   if (false == this->OnInit(argc, argv))
   {
      Log("fail to initialize OnInit");
      return -1;
   }

   SDL_Event e;
   while(_running)
   {
      while(SDL_PollEvent(&e) != 0)
      {
       this->OnEvent(&e);
       if(e.type == SDL_QUIT)
       {
          _running = false;
       }
    }
    this->OnLoop();
    this->Render();
    SDL_Delay(0);
   }

   Log("stopping ... ");
   this->CleanUp();
   return 0;
}


3. Log.h


/*
 Basic macro for logging (can be extended for other target builds; i.e, using
 NSLog for OS X / iOS). Could also be modified to log to a file instead of
 console.

  3/11/2014
    SDLTutorials.com
    Tim Jones
*/

#ifndef LOG_H
#define LOG_H

#include <stdio.h>

#if defined(DEBUG) || defined(_DEBUG)
  #define Log(...) printf(__VA_ARGS__); printf( "\n" );
#else
  #define Log(...) ;
#endif

#endif




  • 제일 처음 할것은 App 클래스를 상속받아 필요한 함수를 구현해 주어야 한다.
  • 이후 명령행 인자들과 함께 Execute() 함수를 호출하여 가동을 시작한다.
  • 가동을 위해 최초 OnInit() 이벤트가 한번 발생하고, 
  • 사용자 입력(키보드, 마우스, 조이스틱 등)이 있을때 마다 OnEvent 가 발생한다.
  • 또한 게임로직등 다양한 일처리는 OnLoop 에서 구현하고
  • 매 프레임 렌더링 코드는 OnRender 에서 구현한다.
  • 프로그램 종료시에는 OnCleanUp 에 의해 자원회수 코드를 구현한다.


다음 글에서는 이를 기반으로 렌더링 데모를 시연해 보도록 하자.


2014년 7월 16일

프로세스 공정 능력 계산

어떤 기계장치의 계측값 내지는 측정되는 데이타를 축적하여 이를 분석하여 공정능력 (cp : capability of process) 이란 값을 산출할수 있다. 이 값은 결국 불량율 즉 수율(yield)등으로 환산할수도 있다.

관련링크 :
http://en.wikipedia.org/wiki/Process_capability_index
http://www.itl.nist.gov/div898/handbook/pmc/section1/pmc16.htm

링크에는 자세한 수식이 나와있으니 이를 기반으로 계산을 하여도 된다. Cpk 가 의미하는 바를 그래프로 보면 아래와 같다.



어쨋든 이 공정능력을 계산해주는 프로그램을 만든다면 다음과 같을 것이다.



이를 구현한 클래스를 소개하는 선에서 설명을 마치고자 한다.


---------------------------------- procap.h --------------------------------------
#ifndef procapH
#define procapH

///
/// copyright to sepwind@gmail.com (http://sepwind.blogspot.com)
///

#include <windows.h>
#include <vector>


class ProcessCapabilityPimpl;
class ProcessCapability
{
public:
 int   size();
    void  clear();
    void  reserve(int count);
    void  push_back(double sample);
    double&  operator[](int index);
 double  mean();
    double  stddev();
    double  solveCp(double lower, double upper);
    double  solveCpk(double lower, double upper);

protected:
    ProcessCapabilityPimpl*  _pPimpl;

public:
 ProcessCapability();
    virtual ~ProcessCapability();
};


#endif

---------------------------------- procap.cpp------------------------------------
#include "procap.h"
#include <cassert>
#include <math.h>

///
/// copyright to sepwind@gmail.com (http://sepwind.blogspot.com)
///

class ProcessCapabilityPimpl
{
public:
 std::vector<double> samples;

    ProcessCapabilityPimpl()
    {}
    ~ProcessCapabilityPimpl()
    {}
};

// ----------------

ProcessCapability::ProcessCapability()
{
 _pPimpl = new ProcessCapabilityPimpl;
}


ProcessCapability::~ProcessCapability()
{
 delete _pPimpl;
    _pPimpl = NULL;
}

int ProcessCapability::size()
{
 return _pPimpl->samples.size();
}

void ProcessCapability::clear()
{
 _pPimpl->samples.clear();
}

void ProcessCapability::reserve(int count)
{
 _pPimpl->samples.reserve(count);
}

void ProcessCapability::push_back(double sample)
{
 _pPimpl->samples.push_back(sample);
}

double& ProcessCapability::operator[](int index)
{
 return _pPimpl->samples[index];
}

double ProcessCapability::mean()
{
 double sum(0.0);
    std::vector<double>::iterator it;
 for(it = _pPimpl->samples.begin(); it != _pPimpl->samples.end(); it++)
    {
  sum += *it;
    }

    return sum / (double)this->size();
}

double ProcessCapability::stddev()
{
 /// sigma

 double m = this->mean();
    int n = this->size();
    double sum(0.0);

    std::vector<double>::iterator it;
 for(it = _pPimpl->samples.begin(); it != _pPimpl->samples.end(); it++)
    {
     sum += pow(*it - m, 2);
    }

    return sqrt(sum / (double)n);
}

double ProcessCapability::solveCp(double lower, double upper)
{
 assert(lower < upper);

 double cp(0.0);
    cp = (upper - lower) / (6.0 * this->stddev() );
    return cp;
}

double ProcessCapability::solveCpk(double lower, double upper)
{
 assert(lower < upper);

 double cpk(0.0);
    double m = this->mean();
    double σ = this->stddev();

    cpk = std::min<double>( \
     (upper - m) / (3.0*σ),
        (m - lower) / (3.0*σ)
        );

    return cpk;
}


원리는 링크에 나온 수식을 구현하여 매우 간단하며, solve cp, solve cpk 함수의 구현부를 참고.

2014년 7월 11일

Undo / Redo 시스템 디자인

본인이 디자인하는 S/W 기능중 Undo/Redo  적용하기 위해서는 여러가지 방법이 있을 것이다. 그중 모든 행위에 대한 추적을 통해 이력을 관리하는 방법을 설명해 보자.


이러한 디자인의 원칙은 다음과 같다.
undo/redo 가 필요한 모든 행위(함수호출)를 관리하기 위해 하나의 인터페이스를 정의하고 이를 상속 구현하여 연결리스트, 스택등의 자료구조에 차곡차곡 저장해 놓는다.






1. Action 인터페이스

  모든 행위(Action)들은 이 인터페이스를 상속받아 undo / redo 동작을 각각 구현한다. undo 는 반드시 redo 가 한 행위를 반대로 돌려놓아야 한다. 예를 들어 ActionMove 의 경우 어떤 대상을 dx, dy 만큼 이동하는 행위이고 redo 에서 dx, dy 만큼을 더했다면, undo 에서는 dx, dy 만큼을 빼도록 구현한다.

2. Action 인터페이스를 상속받은 실제 액션들 ActionMove, Action,...

 Action 인터페이스를 상속받아 undo/redo 를 구현한다. 유저가 메뉴에서 복사, 붙이기 행위를 한다면 각각 복사(ActionCopy) 객체를 생성하여 ActionControl 의 execute에 전달한다. 이후 붙이기(ActionPaste) 명령을 동일하게 execute 시킨다.
결국 ActionControl.execute() 의 내부에서는 전달받은 Action 인스턴스의 redo 함수를 호출해 주는 것에 불과하다.

3. Action 들을 저장/관리 해주는 컨트롤러 ActionControl

 이 녀석이 실제 생성된 모든 액션(Action,...)들을 자료구조(연결리스트, 스택 등 컨셉에 맞도록 선정)에 지속적으로 쌓아 저장한다. 즉 execute() 호출시 undo 를 시키고 _actions 이라는 컨테이너에 이 인스턴스의 포인터를 쌓아 놓다가, 외부에서 되돌리기 기능이 필요하다면 이 _actions 컨테이너에서 가장 앞단의 Action 인스턴스를 꺼내와 undo 를 호출해 주면 되는 것이다.


위와 같이 액션의 저장과 실제 행위를 분리하여 설계하는 것이 흔한 방식이다.

멀티 쓰레드 클래스 디자인

전역 함수를 쓰레드의 진입주소로 지정하여 멀티쓰레딩 시키는 구현하는 방식은 매우 쉽지만, 특정한 클래스의 멤버 함수를 쓰레드로 동작시키려면 몇가지 고려해야 할 사항이 있다.

특히 멤버 함수는 컴파일 시간에 함수의 주소를 알수 없기 때문에, 이를 해소하기 위해 멤버 함수를 static 으로 지정하는등의 꼼수(?)를 사용하기도 한다. 그러나 이럴 경우 static 함수내에서 접근이 가능한 멤버 변수들은 오직 static이어야 하다는 등의 여러 제약사항이 존재한다.

때문에 클래스의 멤버 함수를 쓰레드로 동작시키는 간단한 인터페이스 구조를 사용하여 이를 해결해 보자.


1. 우선 쓰레드로 동작할 녀석일 경우 아래 인터페이스를 상속받도록 한다.


class Threading
{
public:
  virtual int __stdcall execute()=0;
};

즉 사용자가 만든 클래스는 이 인터페이스를 상속받아 execute 를 구현하도록 한다. 당연히 execute 함수내부에서는 this 에 접근이 가능할뿐 아니라,  멀티쓰레딩에 안전하도록 고안하여 구현한다.

2. Threading 인터페이스를 관리하는 녀석을 만들자

class Thread
{
public:
  bool  execute();
  bool  join(DWORD timeout);

  Threading*  _pThreading;

  explicit Thread(Threading* pThreading)
    : _pThreading(pThreading)
  {
  }

  ~Threading();
};


이 녀석은 쓰레드를 시작, 종료대기하는 녀석으로 생성자에서 1번의 인터페이스를 받아 저장한후 execute 에서 쓰레드를 가동시킨다.

실제 구현내용을 살펴보면 ...

unsigned __stdcall FnThread(LPVOID pArg)
{
Thread* pThread = static_cast<Thread*>(pArg);
Threading* pThreading = static_cast<Threading*>(pThread->_pThreading);
        return _pThreading->execute();

}

bool Thread::execute()
{
  HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, FnThread, this, 0, NULL);
  //...
  return true;
}

bool Thread::join(DWORD timeout)
{
  return WAIT_OBJECT_0 = ::WaitForSingleObject(hThread , timeout);
}


정리하면 다음과 같은 순서로 작업하도록 한다.

  • Threading 인터페이스를 상속받아 쓰레드 동작 코드를 구현한다.
  • Threading 인스턴스를 Thread 의 생성자에 지정한다.
  • Threading.execute() 로 쓰레드를 가동한다.
  • 시그널/플래그 등의 조건으로 쓰레드를 정상종료시킨다
  • Threading.join() 로 종료대기및 자원회수를 한다.

ps. 위와 같은 구조는 Java의 Runnable 인터페이스나 .NET 의 프레임 워크와 매우 흡사함을 알 수 있다.


GDI 를 이용한 더블 버퍼링 (double buffering)


GDI 기술은 사실상 수십년도 더된 구닥다리(?) 기술로, GDI+를 넘어 최근 M$ 운영체제는 그래픽 카드의 GPU를 이용해 바탕화면에서 조차 하드웨어 가속을 위해 3D 기반의 라이브러리인 DirectX를 경량화한 Direct2D 을 제공하고 있다.

이 기술은 윈도우 비스타 이후 시리즈만 사용가능하며 Aero Glass 라는 명칭으로 광고를 하고 있다.

구닥다리 기술인 GDI 을 개선하고자 알파블랜딩과 객체지향적인 라이브러리를 구조를 가진 GDI+ 가 있지만, 이번 글에서는 GDI 를 이용한 전통적인 2D 렌더링 방법을 살펴보자.




위 이미지를 보면, 만개의 사각형을 더블버퍼링 기법을 이용하여 렌더링 하고 있으며 약 62msec 이 소요되었음을 알수있다.

이 소스의 핵심사항은 단순히 그리는 기능이 아닌 아래와 같은 추가 기능을 적용하였다.


  • 더블 버퍼링을 사용하여 화면의 깜빡거림(flickering)을 방지
  • 화면의 확대 축소 기능을 제공한다. 즉 줌 효과를 어떤식으로 적용하는지 알수있다.
  • 화면의 원점 위치를 변경한다. 이를 통해 마우스나 멀티터치 입력시 중심위치를 변경가능하다.
  • 좌상단이 0,0 인 윈도우의 기본 좌표계를 X,Y 직교 좌표계로 변경한다.
  • 입력되는 논리좌표계를 사용자 임의대로 변경한다. 즉 여기에서는 논리적으로 1um 를 표현하기 위해 GDI의 그리기 함수의 좌표에 항상 1000 값을 곱한다



------------------------------- test.cpp -------------------------------
DWORD tick = ::GetTickCount();

HWND hwnd = this->Handle;
HDC hdcFront = ::GetDC( hwnd );

RECT rect;
::GetClientRect(hwnd, &rect);
HDC hdc = ::CreateCompatibleDC( hdcFront);
::SetGraphicsMode(hdc,  GM_ADVANCED);
HBITMAP hbitmap = ::CreateCompatibleBitmap(hdcFront, rect.right, rect.bottom);
HBITMAP holdbitmap = (HBITMAP)::SelectObject( hdc, hbitmap);


// clear background with black
::SetROP2(hdc, R2_COPYPEN);
::SetMapMode(hdc, MM_TEXT);
HBRUSH hbrush = ::CreateSolidBrush( RGB(0,0,0) );
HBRUSH holdbrush = (HBRUSH)::SelectObject(hdc, hbrush);
::Rectangle(hdc, 0, 0, this->ClientWidth , this->ClientHeight);
::SelectObject(hdc, holdbrush);
::DeleteObject(hbrush);

// scale and origin location
double scale = 1.0;
POINT center;
center.x = this->ClientWidth /2;
center.y = this->ClientHeight /2;

::SetMapMode(hdc,  MM_ISOTROPIC);
::SetWindowExtEx(hdc, (int)(this->ClientWidth/(scale/1000.f)) , (int)(this->ClientHeight/(scale/1000.f)) , NULL);
::SetViewportExtEx(hdc, this->ClientWidth , -this->ClientHeight, NULL);
::SetViewportOrgEx(hdc, center.x, center.y, NULL);

// draw shapes
srand( ::GetTickCount() );
HPEN hpen = ::CreatePen(PS_SOLID, 0, RGB(rand()%255, rand()%255, rand()%255) );
HPEN holdpen = (HPEN)::SelectObject(hdc, hpen);
for (int i=0; i< 10000; i++)
{
    ::Rectangle(hdc,
        1000*(rand() % 200 -100),
        1000*(rand() % 200 -100),
        1000*(rand() % 200 -100),
        1000*(rand() % 200 -100)
        );

}

::SelectObject(hdc, holdpen);
::DeleteObject(hpen);

////////////////////////////////////////////////////////////////////////////

// revert transformation
::SetMapMode(hdc,  MM_TEXT);
::SetWindowExtEx(hdc, this->ClientWidth , this->ClientHeight, NULL);
::SetViewportExtEx(hdc, this->ClientWidth, this->ClientHeight, NULL);
::SetViewportOrgEx(hdc, 0,0, NULL);

// swapping font/back buffers
::BitBlt(hdcFront, 0, 0,  rect.right, rect.bottom, hdc, 0, 0, SRCCOPY);
::SelectObject(hdc, holdbitmap);

// clean up
::DeleteObject( hbitmap);
::DeleteDC(hdc);
::ReleaseDC( hwnd, hdcFront);

tick = ::GetTickCount() - tick;

ShowMessage("rendering time(msec) " + IntToStr((int)tick));

------------------------------------------------------------------------------


위 소스는 C++ Builder 기반으로 작성되었으며, OnPaint 이벤트에 적용하면 동작이 가능하다. 또한 MFC 나 기타 환경에서도 VCL 함수부분만 삭제하면 API를 통해 동작이 가능하다.
더블 버퍼링 구현을 위한 순서는 아래와 같다.


  • 후면 버퍼(hdc)의 DC에 검은색으로 브러쉬를 칠한다.
  • 후면 버퍼(hdc)의 확대. 중심위치등 변환행렬을 변경한다.
  • 후면 버퍼(hdc)에 각종 그리기 명령을 실시한다. 
  • 후면 버퍼(hdc)의 변환행렬을 초기화 한다.
  • 후면 버퍼(hdc)의 내용을 전면버퍼(hdc front)로 뿌려준다 (bitblt)


기타 현재 마우스가 위치한 픽셀 좌표가 실제 논리좌표로 어떤 위치인지를 알기 위해서는 Mouse Move 이벤트에 아래의 pseudo code 를 사용하면 얻어올수있다.


  • 후면버퍼(hdc)에 적용되었던 변환행렬을 호출한다.
  • DP2LP / LP2DP 등 함수를 이용하여 위치 정보를 역연산하여 얻어온다.


2014년 7월 10일

Freetype 을 이용한 트루타입 폰트 분석

이전 게시글에서 윈도우 폰트의 원리및 해당 경로(BeginPath, EndPath)를 추출하는 API 를 소개한바 있다.


트루타입 폰트


오늘 소개하려는 방식은 오픈 소스인 Freetype 라이브러리를 사용한것으로 이를 이용해 개발자가 다양한 프로그램 예를 들어 폰트 제작, 편집, 뷰어등을 만들수도 있다.

또한 폰트를 각종 인쇄 매체에 프린트하는 기계장치를 제어하는 분야나, 레이저를 이용해 글자를 새기는 장비, 홈메이드 CNC 장치등 활용 범위가 다양하다.




Freetype 를 사용하는 가장 쉬운 방법은 위와같이 당연히 예제를 작성해 보는 것이다.

첨부된 예제에서는 폰트파일을 하나 지정해 폰트를 구성하는 모든 글자들(Glyph)를 순회하면서, 각 글자를 구성하는 모든 좌표정보를 출력하는 간단한 기능을 제공한다.




이같은 Raw 데이타를 이용하면 앞서 제시한 다양한 분야에 적용이 가능하다.

위 캡춰화면은 Arial.ttf 폰트 파일을 분석한 것으로 개별 글자의 구성요소를 보여준다. 즉 이동(MOVE TO), 선분(LINE TO), 그리고 제어점 정보(CONIC) 들이 출력된다. 즉 시작점(p0), 제어점(p1,2), 끝점(p3)이 있다고 가정하면, 아래 pseudo code 로 각 보간(interpolation) 점들을 연산해 낼수 있다.

#define CONIC_B1(t) (1.0-t)*(1.0-t)
#define CONIC_B2(t) 2.0*t*(1.0-t)
#define CONIC_B3(t) (t*t)
// ...
int steps = 100;
double t = 0.0;
double bias = 1.0 / steps;
do
{
  x = p1.x * CONIC_B1(t) + p2.x * CONIC_B2(t) + p3.x * CONIC_B3(t);
  y = p1.y * CONIC_B1(t) + p2.y * CONIC_B2(t) + p3.y * CONIC_B3(t);
  // 해당 x,y 위치정보 이용
  // 화면 출력, 기계장치 제어 등
  t += bias;
}while( t< 1.0);

// ...

위 코드를 살펴보면 결국 곡선을 미분화된 직선으로 보간하는것으로 얼마나 잘게 쪼갤지는 사용자가 지정한 steps 값에 의해 결정된다. 이를 통해 표현되는 곡선의 미려함을 지정할수 있게 될뿐 아니라, 확대 축소할경우 부드러운 곡선을 보간하고자 한다면 이 값을 키우거나 적절히 줄이면 품질, 속도를 조절 할 수 있게 된다.

곡선의 구현에 대한 정보는 다음의 링크에서 상세하게 기술되어 있다 링크 : http://www.codeproject.com/Articles/747928/Spline-Interpolation-history-theory-and-implementa


------------------------ main.cpp 소스--------------------

#include <windows.h>
#include <tchar.h>
#include <iostream>


/// 
/// copyright to sepwind@gmail.com (http://sepwind.blogspot.com)
/// 

#include ".\freetype\include\ft2build.h"
#include FT_FREETYPE_H
#include FT_MODULE_H
#include FT_OUTLINE_H
#include FT_GLYPH_H

#pragma comment(lib, "freetype.lib")


void usage() 
{
std::cout << "Copyright to sepwind 2014. \nblog site: http://sepwind.blogspot.com email: sepwind@gmail.com\n\n";
    std::cout << "Usage : fontconverter.exe <options> <ttf file> <sep file>\n";
    std::cout << "  ttf file : true type font input file\n";
    std::cout << "  sep file : sepwind font output file \n";
}

int moveTo(FT_Vector* to, void* );
int lineTo(FT_Vector* to, void* );
int conicTo(FT_Vector* control, FT_Vector* to, void* );
int cubicTo(FT_Vector* control1, FT_Vector* control2, FT_Vector* to, void* );


static const FT_Outline_Funcs decomposeFunctions = 
{
      (FT_Outline_MoveTo_Func) moveTo,
      (FT_Outline_LineTo_Func) lineTo,
      (FT_Outline_ConicTo_Func)conicTo,
      (FT_Outline_CubicTo_Func)cubicTo,
      0, 
 0
};


int moveTo(FT_Vector* to, void* ) 
{
std::cout << "move to\t\t" << to->x << ",\t" << to->y << std::endl;
    return 0;
}


int lineTo(FT_Vector* to, void* ) 
{
std::cout << "line to\t\t" << to->x << ",\t" << to->y << std::endl;
    return 0;
}


int conicTo(FT_Vector* control, FT_Vector* to, void* ) 
{
std::cout << "conic to\t" << to->x << ",\t" << to->y << "\tcontrol : " << \
control->x << ",\t" << control->y << std::endl;
    return 0;
}


int cubicTo(FT_Vector* /*control1*/, FT_Vector* /*control2*/, FT_Vector* to, void* ) 
{
std::cout << "cubic to\t" << to->x << ",\t" << to->y << std::endl;    
    return 0;
}


BOOL convertGlyph(FT_Face face, FT_ULong charcode)
{
    FT_Error error;
    FT_Glyph glyph;

    // load glyph
    error = FT_Load_Glyph(face,
                          FT_Get_Char_Index(face, charcode),
                          FT_LOAD_NO_BITMAP | FT_LOAD_NO_SCALE);
    if (error) 
{
        std::cerr << "FT_Load_Glyph: " << error << std::endl;
        return FALSE;
    }

FT_Get_Glyph(face->glyph, &glyph);
    FT_OutlineGlyph outlineGlyph = (FT_OutlineGlyph)glyph;
    if (face->glyph->format != ft_glyph_format_outline) {
        std::cerr << "Not an outline font\n";
return FALSE;
    }

///charcode
std::cout << ">>> begin of character code : " << charcode << std::endl;

    // trace outline of the glyph
    error = FT_Outline_Decompose(&(outlineGlyph->outline), &decomposeFunctions, 0);
    //error = FT_Outline_Decompose(&(outlineGlyph->outline), &decomposeFunctions, 0);

    if (error) 
{
        std::cerr << "FT_Outline_Decompose: " << error << std::endl;
        return FALSE;
    }
std::cout << "<<< end of character code : " << charcode << std::endl << std::endl;

return TRUE;
}


int _tmain(int argc, _TCHAR* argv[])
{
if (argc != 3)
{
usage();
return -1;
}

std::string inputfile = argv[1];
std::string outputfile = argv[2];    
    std::cout << "input file: " << inputfile.c_str() << "\n";
    std::cout << "output file: " << outputfile.c_str() << "\n";

FT_Library library;
FT_Face face;

FT_Error error = FT_Init_FreeType(&library);
    if (error) 
{
        std::cerr << "FT_Init_FreeType error : " << error << std::endl;
        return -1;
    }

error = FT_New_Face(library,
inputfile.c_str(),
                        0,
                        &face);
    if (error) 
{
        std::cerr << "FT_New_Face error : " << error << std::endl;
std::cerr << "please verify the input font filename and path " << std::endl;
return -1;
    }
        
    std::cout << "family:    " << face->family_name << "\n";
    std::cout << "height:    " << face->height << "\n";
    std::cout << "ascender:  " << face->ascender << "\n";
    std::cout << "descender: " << face->descender << "\n";
    
unsigned first = 0;
FT_Get_First_Char(face, &first);

FT_ULong  charcode;
    FT_UInt   gindex = 0;

    // iterate through glyphs
    charcode = FT_Get_First_Char( face, &gindex );
    while (gindex != 0) {
if (!convertGlyph(face, charcode))
break;
        charcode = FT_Get_Next_Char(face, charcode, &gindex);
    }

    FT_Done_Face(face);
    FT_Done_Library(library);
return 0;
}


ps1. Freetype 라이브러리를 기반으로 OpenGL 렌더링이 필요한 분들은 FTGL 라이브러리를 참고

ps2. 곡선에 대해 좀더 자세한 정보를 얻고자 한다면 "베지어 곡선" 으로 검색


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

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