home lecture link bbs blame

◈12 OpenGL ES 1.1◈

12.1 OpenGL ES 개요

OpenGL ES Work Station용으로 설계된 OpenGL Embedded System 환경에 맞게 경량화 시킨 그래픽 API입니다. OpenGL OpenGL ARB(Architecture Review Board)에서 관리하고 OpenGL ES는 크로노스 그룹(Khronos Group)에 관리합니다.

 

<크로노스 그룹, OpenGL ARB>

 

)크로노스 그룹은 오디오, 비디오, 그래픽 등의 미디어에 관련된 로열티가 없는 개방형 표준 API를 제정하고 가속을 위해서 구성된 비영리 컨소시엄 그룹입니다. 2000년에 3DLabs, Discreet, Evans & Sutherland, Intel, Nvidia, SGI, ATI 등 그래픽 및 미디어 관련 회사 중심으로 설립 되어 현재는 약 100개의 업체가 참여하고 있으며 우리나라의 삼성, SK, LG 등도 참여하고 있는 그룹입니다.

OpenGL ES 이외에 크로노스 그룹에서 관리하고 있는 대표적인 미디어 API는 임베디드 시스템에서 벡터 그래픽 기능을 위한 OpenVG (Open Vector Graphics), 미디어 처리의 표준인 OpenMAX (Open Media Acceleration Primitives), 그리고 임베디드 시스템의 사운드와 관련된 OpenSL ES (Open Sound API form Embedded System) 등이 있습니다.

 

<크로노스 그룹에서 관리하는 API>

OpenGL ES을 포함한 크로노스 그룹에서 관리하는 API의 가장 큰 특징은 전 세계 수십 개 이상의 업체가 참여해서 끊임없는 표준화 노력으로 이식성이 상당히 높으며 또한 Open API로서 개발에 대한 특별한 라이센스 비용을 지불하지 않아도 됩니다.

그리고 시스템의 안전을 최우선으로 설계되어 있어서 휴대 전화, PDA, 자동차, 항공기 등과 같은 Embedded 환경에서 요구되는 저 전력, 저 메모리에서 안전한 프로그램을 만들 수가 있습니다.

특히, OpenGL ES는 플랫폼 독립으로 설계되어 있어서 OpenGL ES를 지원하는 환경이라면 PC에서 개발하고 Embedded 환경에서 2D, 3D 게임을 마음껏 표현할 수가 있습니다.

 

OpenGL ES API 함수들은 Workstation OpenGL의 함수 또는 기능을 제거한 Minor 판이라 할 수도 있는데 이 API Mobile, Embedded 환경에서도 간단하게 사용되면서도 높은 품질을 유지하는 것이 목표이기 때문에 Work station 용 라이브러리에서 잘 사용이 안되거나 Embedded 환경에서 필요하지 않는 함수들은 포함되지 않게 되었습니다. 또한 일부 플랫폼들은 표준 OpenGL ES 함수 이외에 자신의 하드웨어 성능을 올릴 수 있는 함수들을 ARB와 비슷한 형태로 제공해서 수준 높은 화면의 품질을 구성할 수 있도록 도와주기도 합니다.

현재, 3D 가속 칩 셋이 포함된 거의 모든 모바일 기기들은 OpenGL ES를 지원하고 있습니다. NVIDIA, AMD, Power VR의 경우, 모바일 뿐만 아니라 PC에서도 작업할 수 있는 라이브러리를 제공하고 있어서 여러분이 어느 정도 OpenGL에 대한 개념만 있으면 모바일에서도 쉽게 적응할 수 있도록 SDK와 예제들이 잘 구성되어 있습니다. 그리고 OpenGL ES에 대한 수 많은 문서, 예제, 토론 그룹들이 전 세계에 존재하고 있어서 PC 환경에서 개발하고 Embedded 기기에 이식하기가 무척 수월합니다.

이처럼 OpenGL ES Workstation용과 호환성이 어느 정도 있어서 PC에서 개발하고 Embedded용으로 쉽게 이식성이 높으며, 수많은 개발자와 커뮤니티의 노력으로 안정적이고, 범용적인 그래픽 라이브러리로 자리잡고 있어서 OpenGL ES 사용은 현재의 Embedded 환경에서 3D 게임을 만들기 위해서 가장 훌륭한 선택이라 할 수 있습니다.

 

간단히 OpenGL ES의 특징을 열거해보았는데 본격적으로 OpenGL ES 대해서 알아 봅시다. Embedded OpenGL ES는 크게 1.x버전과 2.x 두 종류가 있는데 현재 가장 많이 사용되는 1.x에 대해서 살펴보고 다음, 프로그램 가능한 파이프라인으로 구성된 2.X을 중심으로 강의를 진행하겠습니다.

 

 

12.2 EGL(Embedded-System Graphics Library) Framework

-EGL 개요-

OpenGL ES 3D 그래픽 처리에 대한 API 입니다. 그런데 대상 기기들 모두가 같은 환경, 같은 성능을 가질 수는 없습니다. 따라서 OpenGL ES가 실행될 수 있도록 일정한 환경을 만들어 주어야 합니다. 앞서 우리는 Windows 환경에서 OpenGL를 사용하기 위해서 wgl로 시작되는 위글(wgl) 함수들을 사용해서 OpenGL을 사용할 수 있도록 환경을 만들었습니다. 마찬 가지로 Embedded 환경에서도 위글 함수들과 같이 환경을 조성하는 함수들이 필요한데 이 역할을 수행하도록 권장하는 것이 EGL입니다.

크로노스 그룹에서 EGL (Embedded-System Graphics Library) Embedded 시스템의 그래픽 라이브러리로서 OpenGL ES 또는 OpenVG와 같은 Khronos rendering API와 시스템 하부의 렌더링 사이에 있는 interface로 정의하고 있습니다. 구체적으로 EGLgraphics context 관리하고, 서피스와 버퍼를 바인딩하며, 렌더링 동기화를 제어 합니다. 또한 고 성능 렌더링 하드웨어 가속, 다른 Khronos APIs를 사용한 2D 또는 3D에 대한 혼합 모드 렌더링을 만들어 냅니다.

EGL은 일종의 권장사항이기 때문에 아이폰(iPhone)과 같이 EGL을 대신 다른 형태로 렌더링 상태를 만드는 경우도 있습니다. 하지만 EGL이 지원이 되는 플랫폼이라면 PC에서 사용한 코드를 수정 없이 그대로 Embedded 기기에 올릴 수 있기 때문에 편의를 위해서 대부분의 플랫폼 제조사는 이를 지원하고 있습니다. 때로는 Embedded 기기만 지원되기도 하지만 이런 경우에도 수정이 필요한 부분이 몇 줄 안되기 때문에 큰 불편은 없습니다. 참고로 리눅스에서도 EGL, WGL에 해당하는 GLX가 존재합니다. GLX X-window 환경에서 구현이 되며 사용법은 EGL, WGL과 거의 같으므로 알아두는 것이 좋습니다.

 

-OpenGL ES SDK-

먼저 여러분은 OpenGL ES를 기반으로 프로그램을 만들기 위해서 SDK를 설치해야 합니다. Mac을 가진 분들이라면 아이폰 SDK를 설치하면 됩니다. Nvidia, ADM, Imagination과 같은 그래픽 제조사들은 OpenGL ES를 위한 SDK를 지원하고 있는데 Nvidia 사는 SDK 사용 승인 절차가 필요하고 자사의 그래픽 카드에서만 동작으로 하도록 구성되어 있습니다. AMD OpenGL ES 2.0만 지원되지만 공개되어 있어서 사용하기 좋았는데 얼마 전에 AMD 사이트에서 링크를 해제해 버렸습니다. Imagination OpenGL 1.0, 1.1, 2.0 에 대한 SDK를 배포하고 있을 정도로 다른 어느 회사보다 OpenGL ES에 대해서 가장 적극적으로 지원하는 회사라 할 수 있습니다.

국내의 경우 CAANOO를 만든 GPH (Game Park Holdings) 회사에서 GPH SDK 안에 윈도우, 리눅스, CAANOO OpenGL ES를 포함해서 배포하고 있습니다. GPH SDK는 얼마 전까지 AMD와 동일하게 PC 개발 환경에서는 OpenGL 1.5를 사용했다가 최근에 그래픽 칩 셋을 Imagination으로 바꾸면서 SDK에 포함된 OpenGL ES 라이브러리도 바꾸었습니다.

GPH 이외에 삼성에서 bada OS에도 OpenGL ES가 포함되어 있습니다. 다소 복잡한 namespace Eclipse를 개조한 IDE c/c++ 개발자에게 이질감을 주지만 이것은 bada가 개발자의 편의를 위해서 nucleus tool chain와 윈도우 환경을 통합했기 때문입니다. 이외에 닌텐도 등도 비공식적인 SDK가 인터넷에 존재하고 있고, 안드로이드의 NDK를 사용한다면 OpenGL ES를 충분히 실습할 수 있습니다.

이중에서 추천하고 싶은 SDK로는 Imagination, GPH, AMD라 할 수 있습니다. 특히, GPH SDK는 게임용으로 특화된 DGE 라이브러리가 있고 이 라이브러리는 기기뿐만 아니라 PC에서도 간단한 게임을 만들 수 있도록 그래픽스, 인풋, 폰트, 사운드, 동영상 재생, 네트워크, 리소스 패킹 등이 잘 구성되어 있습니다. DGE 이외에 SDL도 포함되어 있어서 처음으로 게임을 개발하는 사람들에게 여러모로 유용한 라이브러리를 제공하고 있습니다.

지금까지 OpenGL ES SDK들을 아주 간단히 소개해 드렸는데 좀더 자세한 내용은 부록에 포함된 SDK 주소에서 찾기 바라며 앞으로 진행될 OpenGL ES 1.1Imagination SDK GPH SDK를 사용하겠습니다.

 

-EGL 프로그래밍-

PC에서의 OpenGL 사용에서 우리는 WGL의 가장 중요한 역할은 Rendering Context관리라 배웠습니다. EGL도 마찬가지로 가장 큰 목적은 OpenGL ES 라이브러리를 사용할 수 있도록 Rendering에 필요한 Context, Surface 등을 만드는 것입니다. 따라서 프로그램의 흐름도 WGL과 유사할 수밖에 없으며 단지 차이라면 EGL WGL보다 좀 더 세밀하게 나뉘어져 있다는 것입니다. 나중에 다 배우고 나서 필요하다면 기억하기 좋게 WGL EGL을 표로 만들어서 비교해 보는 것도 좋습니다.

 

어떤 EGL 라이브러리를 사용하더라도 여러분은 반드시 렌더링 대상이 되는 EGL이 요구하는 Native Window를 만들어야 합니다. 여러분은 다음과 비슷한 형태로 플랫폼에 의존하는 윈도우(Native Window)를 만들 것입니다.

 

<윈도우 생성>

hwnd = CreateWindow();                               // window

hwnd = malloc(16*1024);                                      // GPH SDK

hwnd = new MyForm : public Osp::Ui::Controls::Form;  // Form을 상속받는 객체

hwnd = android_createDisplaySurface();                       // android

 

간단히 윈도우 생성을 설명하면 마이크로소프트 계열은 윈도우 생성을 CreateWindow() 함수로 만듭니다. GPHCAANOO는 기기가 고정되어 있어서 추가로 Heap16*1024 영역의 공간만 만들어 주고 이 메모리를 EGL로 넘기면 됩니다. bada 플랫폼은 BaseForm 객체이고 이 객체가 윈도우를 대신하고 있어서 폼 객체를 윈도우로 캐스팅하면 됩니다. 안드로이드는 애플리케이션 영역에서 자동으로 생성 됩니다. 특히 진저(2.3) 버전 이후는 Native Activity를 사용해서 JAVA 없이도 응용 프로그램을 만들 수 있게 되었고 이 Activity Life Cycle에서 만들어진 윈도우를 캐스팅해서 사용합니다.

 

이렇게 직접 윈도우를 만들거나 아니면 자동으로 생성된 윈도우를 가지고 우리는 다음으로 EGL에 따른 디스플레이(EGL Display), 렌더링 서피스(EGL Rendering Surface), 렌더링 컨텍스트(Rendering Context)를 생성하고 장면의 연출에서 후면 버퍼를 전면 버퍼로 전환인 Flipping, 그리고 EGL 관련 객체들을 해제(Release) 과정을 알아야 합니다. EGL 객체들의 생성은 아이폰을 제외한 나머지 플랫폼에서는 거의 비슷한 코드로 다음과 같은 순서로 구성되어 있습니다.

 

1. Display 객체 생성

2. Display 객체 초기화

3. 기기에서 지원되는 프레임 버퍼(색상, 깊이, 스텐실) 포맷 얻기

4. 서피스(Rendering Surface) 생성

5. 컨텍스트(Rendering Context: RC) 생성

6. 컨텍스트 활성화

 

EGL을 사용해서 RC를 만드는 과정은 플랫폼 마다 거의 비슷하고, OpenGL ES Major 버전이 바뀌더라도 크게 변함이 없어서 굳이 외울 필요는 없고 여러분 자신이 만든 잘 정리된 코드를 가지고 있다가 필요하면 복사해서 사용하는 것이 좋습니다.

 

여러분은 EGL OpenGL을 사용하기 위해서 다음과 같은 형태로 gl.h, egl.h 두 개의 헤더 파일을 포함해야 합니다.

 

#include <gles/gl.h>

#include <gles/egl.h>

 

렌더링에 필요한 EGL 객체는 다음과 같이 Native Window, Display, Surface, Context 4가지가 필요합니다. 이들 객체에 대한 변수를 다음과 같이 설정합니다.

 

NativeWindowType m_pEgWnd = NULL;            // Native Window

EGLDisplay     m_pEGDsp = EGL_NO_DISPLAY;    // 디스플레이 객체

EGLSurface     m_pEGSrf = EGL_NO_SURFACE;    // 서피스 객체

EGLContext     m_pEgCtx = EGL_NO_CONTEXT;    // 렌더링 컨텍스트 객체

 

디스플레이(Native Display) 객체 생성은 eglGetDisplay() 함수를 사용합니다. 이 함수는 실패하면 NULL 객체를 반환합니다.

 

m_pEgDsp = eglGetDisplay(EGL_DEFAULT_DISPLAY);

if(NULL == m_pEgDsp)

{

        printf("eglGetDisplay() Failed\n");

        return -1;

}

 

디스플레이 객체는 생성 후에 반드시 초기화를 해야 합니다. eglInitialize() 함수는 EGL 디스플레이 객체를 초기화 하며 실패하면 "false"를 반환 합니다. 초기화 과정에서 한 부수적으로 EGL 버전을 알아 낼 수 있습니다.

 

EGLint  ver_maj;       // EGL 주 버전

EGLint  ver_min;       // EGL 부 버전

if (!eglInitialize(m_pEgDsp, &ver_maj, &ver_min))

{

        printf("eglInitialize() Failed\n");

        return -1;

}

 

디스플레이 객체를 초기화 한 후에 여러분이 원하는 프레임 버퍼의 포맷이 디스플레이 객체에서 지원 되는지 eglChooseConfig() 함수로 확인 해야 합니다. 이 때 사용하는 단위는 비트입니다. 대부분의 기기들은 가장 무난한 565(red, green, blue) 색상 버퍼와 16-bit 깊이 버퍼를 지원합니다. 하지만 여기서는 좀 더 해상도가 좋은 '8888-16' mode의 지원 여부에 대한 확인 코드를 작성해 보겠습니다.

 

struct _Tconfig_att { EGLint a; EGLint b; };

const _Tconfig_att config_attrib[] =

{

        { EGL_RED_SIZE,             8},               // 8-bit 빨강

        { EGL_GREEN_SIZE,    8},              // 8-bit 초록

        { EGL_BLUE_SIZE,     8},              // 8-bit 파랑

        { EGL_ALPHA_SIZE,    8},              // 8-bit 알파

        { EGL_DEPTH_SIZE,   16},              // 8-bit 깊이

        { EGL_SURFACE_TYPE, EGL_WINDOW_BIT},  // 서피스 타입

        { EGL_NONE,         EGL_NONE      },  // End Mark

};

 

이렇게 작성된 프레임 버퍼의 지원 여부는 eglChooseConfig() 함수를 다음과 같이 사용합니다. 이 함수는 디스플레이에 사용자가 지정한 프레임버퍼의 색상, 깊이 등이 지원 되는지 판단하는 함수이며 성공하게 되면 주어진 조건에 맞는 개수(matching number)를 반환합니다.

eglChooseConfig 함수는 실패하면 "false" 또는 0을 반환합니다. 또한 함수가 처리를 성공하더라도 matching 되는 값이 0이 될 수 있어서 함수의 반환 matching 값 둘 다 확인하도록 합니다.

 

const int      MAX_CONFIG = 16;

int            match;

EGLConfig      eglConf[MAX_CONFIG]={0};

 

if (!eglChooseConfig(m_pEgDsp

               , (const EGLint*)config_attrib, &eglConf[0], MAX_CONFIG, &match))

{

        printf("eglChooseConfig() Failed\n");

        return -1;

}

if(1>match) // No Matching

{

        printf("eglChooseConfig() Failed\n");

        return -1;

}

 

게임기로 특화된 Embedded 기기들은 색상버퍼와 깊이 버퍼가 특정 값으로 고정되어 있는 경우도 있습니다. CAANOO의 경우 8888-16bit 또는 565-16bit로 고정되어 있어서 서피스 타입만 확인하는 config를 작성하면 됩니다.

 

struct _Tconfig_att { EGLint a; EGLint b; };

const _Tconfig_att config_attrib[] =

{

        { EGL_SURFACE_TYPE, EGL_WINDOW_BIT },

        { EGL_NONE,          EGL_NONE      },

};

 

디스플레이에서 사용자가 정의한 포맷이 지원이 확인 되면 다음으로 렌더링을 위한 서피스 객체를 eglCreateWindowSurface() 함수로 생성합니다. 이 함수는 성공하면 서피스 객체를 반환하고 실패하면 NULL을 반환합니다.

 

m_pEgSrf = eglCreateWindowSurface(m_pEgDsp, eglConf[0]

               , m_hWnd, (const EGLint*)config_attrib);

if(NULL == m_pEgSrf)

{

        // 주어진 환경 변수로 실패할 경우 default로 다시 시도

        m_pEgSrf = eglCreateWindowSurface(m_pEgDsp, eglConf[0], m_hWnd, 0);

        if(NULL == m_pEgSrf)

        {

               printf("eglCreateWindowSurface() Failed\n");

               return -1;

        }

}

 

디스플레이, 서피스 객체 생성 다음으로 렌더링 컨텍스트를 생성합니다. 렌더링 컨텍스트 또한 서피스와 마찬가지로 사용자가 eglChooseConfig() 함수에서 지원되는 config 값을 사용해서 만듭니다. eglCreateContext() 함수는 컨텍스트 객체를 생성하며 실패할 경우 'NULL'을 반환합니다.

 

m_pEgCtx = eglCreateContext(m_pEgDsp, eglConf[0]

               , EGL_NO_CONTEXT, (const EGLint*)config_attrib);

if(NULL == m_pEgCtx)

{

        // 주어진 환경 변수로 실패할 경우 default로 다시 시도

        m_pEgCtx = eglCreateContext(m_pEgDsp, eglConf[0], NULL, NULL);

        if(NULL == m_pEgCtx)

        {

               printf("eglCreateContext() Failed\n");

               return -1;

        }

}

 

OpenGL ES는 워크스테이션용 OpenGL과 비슷하게 여러 렌더링 컨텍스트 생성할 수 있고 이 중에서 하나의 컨텍스트를 선택해서 사용할 수 있습니다. eglMakeCurrent() 함수는 현재 활성화되어 있는 디스플레이, 서피스 객체에 사용할 컨텍스트를 지정하는 함수입니다.

 

if(!eglMakeCurrent(m_pEgDsp,m_pEgSrf,m_pEgSrf,m_pEgCtx)) // 서피스에 컨텍스트 연결

{

        printf("eglMakeCurrent() Failed\n");

        return -1;

}

 

보통 렌더링 횟수는 모니터의 재생률(Refresh rate)에 묶여 있는 경우가 많습니다. 대부분의 게임프로그램은 모니터의 재생률에 관계없이 화면에 출력되는 즉시 모드(immediately mode)를 대부분 선택합니다.

eglSwapInterval() 함수는 모니터 동기화 또는 즉시 모드를 설정하는 함수입니다. 이 함수는 모든 기기가 지원하지는 않고 경우에 따라 에러 반환 없이 기기가 멈출 수도 있으니 주의해서 사용하기 바랍니다.

 

if(!eglSwapInterval(m_pEgDsp, 0)) // 즉시 그리기 설정 ==> 0

{

        printf("eglSwapInterval() Failed\n");

        return -1;

}

 

안드로이드 플랫폼의 경우 초기화 과정에서 렌더링 서피스의 폭과 너비를 가져와 뷰포트(Viewport)를 초기화 할 수도 있습니다. eglQuerySurface() 함수는 서피스의 정보를 반환하며 이 함수를 사용해서 뷰포트를 설정합니다.

 

eglQuerySurface(m_pEgDsp, m_pEgSrf, EGL_WIDTH,  &m_ScnW);// 서피스 너비

eglQuerySurface(m_pEgDsp, m_pEgSrf, EGL_HEIGHT, &m_ScnH);// 서피스 높이

glViewport(0, 0, m_ScnW, m_ScnH);                    // 뷰포트 설정

 

이렇게 EGL 객체 생성이 성공했으면 다음으로 Flipping 작업을 정리해야 합니다. OpenGL ES는 간단히 eglSwapBuffers() + glFlush() 함수를 사용해서 Flipping 을 수행합니다.

 

eglSwapBuffers(m_pEgDsp, m_pEgSrf);

glFlush();

 

생성 과정과 더불어 중요한 것이 EGL 객체의 해제입니다. EGL 객체는 해제하기 전에 먼저 디스플레이 객체에 대해서 렌더링 서피스와 컨텍스트를 무효(NULL 또는 No Surface, No Context)로 지정해야 합니다. 그리고 나서 eglDestroyContext (), eglDestroySurface(), eglTerminate() 함수를 사용해서 컨텍스트, 서피스, 디스플레이 순으로 생성과 반대의 과정으로 해제합니다.

 

// 컨텍스트 무효 설정

eglMakeCurrent(m_pEgDsp, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);

eglDestroyContext(m_pEgDsp, m_pEgCtx);       // 렌더링 컨텍스트 해제

eglDestroySurface(m_pEgDsp, m_pEgSrf);       // 서피스 해제

eglTerminate(m_pEgDsp);                      // 디스플레이 해제

 

지금까지 가장 일반적인 방법으로 OpenGL ES API를 사용할 수 있도록 디스플레이, 서피스, 렌더링 컨텍스트 객체를 생성하고 환경을 만드는 방법을 살펴보았습니다. 다음으로 아이폰용 EAGL입니다.  아이폰에 관련된 많은 서적이 있어서 여기서는 간단하게 넘어가도록 하겠습니다.

 

아이폰용 EAGL은 윈도우 서피스 생성은 View 객체 내부에서 자동으로 처리되며 프로그램은 렌더링 컨텍스트를 생성하는 것부터 시작합니다. 아이폰에서 OpenGL ES API를 사용하려면 렌더링 컨텍스트 이외에 프레임 버퍼(Frame buffer)와 색상 버퍼(Color buffer)를 만들어야 합니다. 이들 객체는 OpenGL ES의 텍스처 객체, 버텍스 버퍼 객체처럼 unsigned int 형으로 선언합니다.

 

unsigned int m_fbFrame = 0;   // 디폴트 프레임 버퍼

unsigned int m_fbColor = 0;   // 렌더링에 대한 색상 버퍼

 

아이폰에서 OpenGL 라이브러리를 편하게 사용하려면 Object-C 기반의 X-code 라이브러리를 사용해야 합니다. 다행히도 여러분은 소스 파일의 확장자를 '.c' .m '.cpp' 대신 '.mm'으로 변경하면 Object-C에서 c/c++ 문법과 X-code의 라이브러리를 사용할 수 있습니다.

EAGL 라이브러리를 사용하기 위해서 확장자를 '.m' 또는 '.mm'으로 바꾼 후에 헤더 파일을 #include 대신 Object-C Directive'#import'를 사용해서 EAGL, ES 등을 포함 합니다.

 

#import <QuartzCore/QuartzCore.h>

#import <OpenGLES/EAGL.h>

#import <OpenGLES/ES1/gl.h>

#import <OpenGLES/ES1/glext.h>

 

아이폰은 디스플레이, 윈도우 서피스 없이 Main View를 가지고 바로 렌더링 컨텍스트를 만들기 때문에 Main View가 생성되고 초기화 될 때 렌더링 컨텍스트 객체를 생성하면 됩니다.

여러분은 X-code의 프로젝트 생성 위저드(Wizard)OpenGL ES용 응용프로그램을 생성하면 Main View 코드에 "initWithCoder:(NSCoder*)coder" 함수 안에서 렌더링 컨텍스트 생성에 대한 다음과 같은 코드를 볼 수 있습니다.

 

EAGLContext* pEgCtx = NULL;

pEgCtx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];

 

최신 SDKOpenGL ES 2.0 기반으로 코드가 구성되어 있어서 kEAGLRenderingAPIOpenGLES2로 되어 있습니다. 1.x 버전 EAGL를 사용하려면 숫자 2 1로 바꾸어 kEAGLRenderingAPIOpenGLES1으로 변경합니다.

1.x 버전에서 아이폰의 EAGL 관련 함수와 상수는 "OES" 또는 "_OES" 접미사가 붙습니다. 위저드에서 2.x 기반으로 작성된 코드를 1.x으로 변경할 경우 kEAGLRenderingAPIOpenGLES1으로 변경한 후에 문법 에러를 찾아가면서 "OES" 접미사를 붙이면 됩니다.

 

렌더링 컨텍스트 생성이 성공했다면 eglMakeCurrent() 함수 사용할 때와 비슷하게 이 컨텍스트를 활성화 해야 합니다. 간단히 setCurrentContext 함수에 컨텍스트 객체를 전달하면 됩니다.

 

 [EAGLContext setCurrentContext:pCtx];

 

컨텍스트 객체 생성 후에 다음으로 프레임 버퍼, 색상 버퍼를 생성합니다.

 

glGenFramebuffersOES(1, &m_fbFrame);

glGenRenderbuffersOES(1, &m_fbColor);

 

이들 프레임 버퍼, 색상버퍼를 현재의 Context에 바인딩 하고, 색상 버퍼를 프레임 버퍼에 연결합니다.

 

glBindFramebufferOES(GL_FRAMEBUFFER_OES, m_fbFrame);

glBindRenderbufferOES(GL_RENDERBUFFER_OES, m_fbColor);

glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES

               , GL_COLOR_ATTACHMENT0_OES, GL_RENDERBUFFER_OES, m_fbColor);

 

때로는 EGL 성공을 확인하기 위해서 glClearColor(), glClear(), glSwapBuffers() 함수를 호출해서 화면의 색상이 변경되는지 눈으로 확인할 수 있도록 이 함수들을 추가할 수도 있는데

이 경우 기기가 에러 반환 없이 멈출 수 있습니다. OpenGL ES API 사용은 EAGL Layer 객체를 렌더링 컨텍스트 객체에 전달한 후에 사용하는 것이 가장 안전합니다.

 

EAGL Layer 객체 전달 시점은 화면이 Resize될 때 전달합니다. View 원시 코드 안에 "- (void)layoutSubviews" 함수 안에 resize 될 때 Layer 객체를 컨텍스트 객체에 전달하고 색상 버퍼, 프레임 버퍼를 다시 바인딩 하고 있음을 볼 수 있습니다. EAGL를 상세히 아는 것도 좋지만 내공이 쌓이면 그 때 다시 공부하고 대충 다음과 같이 정리해서 사용합니다.

 

EAGLContext* pCtx  = (EAGLContext*)pEgCtx;

CAEAGLLayer* layer = (CAEAGLLayer*)pLayer;

// 컨텍스트에 레이어 연결

[pCtx renderbufferStorage:GL_RENDERBUFFER_OES fromDrawable:layer];

if(glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES) != GL_FRAMEBUFFER_COMPLETE_OES)

        return -1;

 

EAGL의 렌더링 컨텍스트, 프레임 버퍼, 색상 버퍼를 생성하고 레이어를 컨텍스트에 연결하는 방법을 간단히 살펴 보았습니다.

 

EAGL은 생성에서도 차이가 있듯이 Flipping에도 다른 EGL과 차이가 있습니다.

 간단히 알아보았는데 EAGL는 다른 일반적인 EGL과 차이가 하나 더 있습니다. 보통의 EGL는 워크스테이션용 OpenGL처럼 전면버퍼와 후면 버퍼 교체를 eglSwapBuffers() 함수를 사용하지만 EAGLEAGLContext presentRenderbuffer 함수를 사용합니다. 물론 프레임 버퍼, 색상 버퍼가 렌더링 컨텍스트에 바인딩 된 상태에서 입니다.

 

[pCtx presentRenderbuffer:GL_RENDERBUFFER_OES];

 

아이폰도 비슷하게 먼저 프레임 버퍼, 색상 버퍼를 해제하고 현재의 렌더링 컨텍스트를 nil로 설정하고 나서 컨텍스트 객체를 해제합니다.

 

glDeleteFramebuffersOES(1, &FrameBuffer);    // 프레임 버퍼 해제

glDeleteRenderbuffersOES(1, &ColorBuffer);   // 색상 버퍼 해제

 

if([EAGLContext currentContext] == pCtx)     // 활성화된 서피스, 해제할 서피스 비교

        [EAGLContext setCurrentContext:nil]; // 컨텍스트 무효 설정

 

[pCtx release];                                     // 뷰포트 설정

 

es00_egl_iphone_init1.zip

 

 

OpenGL ES는 아이폰 이외에 3D가 지원되는 Embedded 기기에 대부분 지원 됩니다. 만약 여러분이 Windows, Linux, 아이폰, 안드로이드 등의 Cross Compile를 생각하고 있다면 아이폰 때문에 모든 확장자를 .mm으로 변경할 필요는 없습니다.

여러분은 EAGL이 사용되는 부분만 mm 파일로 변경하면 되며, EAGL에 해당하는 부분을 Destroy, Create, Resize, Present 네 개의 함수로 만들고 이것을 빌드 하도록 구성합니다.

 

#if defined(__APPLE__) || defined(__IPHONE__)

  #import <QuartzCore/QuartzCore.h>

  #import <OpenGLES/EAGL.h>

  #import <OpenGLES/ES1/gl.h>

  #import <OpenGLES/ES1/glext.h>

#endif

typedef unsigned int        UINT;

// EAGL 객체 해제 함수

void XcodeEglDestroy(void** pEgCtx, UINT* pFrameBuffer, UINT* pColorBuffer)

{

        EAGLContext* pCtx = (EAGLContext*)(*pEgCtx);

 

        if(*pFrameBuffer)      // Frame buffer 해제

               glDeleteFramebuffersOES(1, pFrameBuffer);

 

        if(*pColorBuffer)      // Color buffer 해제

               glDeleteRenderbuffersOES(1, pColorBuffer);

 

        if(pCtx)

        {

               // 렌더링에 설정되어 있는 context가 주어진 context 이면 NULL로 설정

               if([EAGLContext currentContext] == pCtx)

                       [EAGLContext setCurrentContext:nil];

 

               // Context 해제

               [pCtx release];

        }

 

        // 주어진 값들을 0 또는 NULL 로 초기화

        *pColorBuffer = 0;

        *pFrameBuffer = 0;

        *pEgCtx = NULL;

}

 

// EAGL 객체 생성 함수

int XcodeEglCreate(void** pEgCtx, UINT* pFrameBuffer, UINT* pColorBuffer)

{

        EAGLContext* pCtx = NULL;

 

        // View 객체에서 Context 얻기

        pCtx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];

        if(!pCtx)

               return -1;

 

        // 렌더링의 Context 설정

        if(![EAGLContext setCurrentContext:pCtx])

               return -1;

 

        // 프레임 버퍼, 색상 버퍼 생성

        glGenFramebuffersOES(1pFrameBuffer);

        glGenRenderbuffersOES(1, pColorBuffer);

 

        // 렌더링 Context에 바인딩

        glBindFramebufferOES(GL_FRAMEBUFFER_OES, *pFrameBuffer);

        glBindRenderbufferOES(GL_RENDERBUFFER_OES, *pColorBuffer);

 

        // 프레임 버퍼에 색상 버퍼 바인딩

        glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES

               , GL_COLOR_ATTACHMENT0_OES, GL_RENDERBUFFER_OES, *pColorBuffer);

 

        *pEgCtx = pCtx;

        return 0;

}

 

// View Resizing

int XcodeEglResize(int* ScnW, int* ScnH, void* pLayer, void* pEgCtx

        , UINT FrameBuffer, UINT ColorBuffer)

{

        EAGLContext* pCtx  = (EAGLContext*)pEgCtx;

        CAEAGLLayer* layer = (CAEAGLLayer*)pLayer;

        glBindRenderbufferOES(GL_RENDERBUFFER_OES, ColorBuffer);

 

        // 렌더링의 Context에 레이어 설정

        [pCtx renderbufferStorage:GL_RENDERBUFFER_OES fromDrawable:layer];

 

        // 렌더링의 Context 설정

        glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES

                       , GL_RENDERBUFFER_WIDTH_OES, ScnW);

        glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES

                       , GL_RENDERBUFFER_HEIGHT_OES, ScnH);

 

        if(glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES) !=

                       GL_FRAMEBUFFER_COMPLETE_OES)

               return -1;

 

        [EAGLContext setCurrentContext:pCtx];

        glBindFramebufferOES(GL_FRAMEBUFFER_OES, FrameBuffer);

        glViewport(0, 0, *ScnW, *ScnH);

        return 0;

}

 

// Present(eglSwapBuffers)

void XcodeEglPresent(void* pEgCtx)

{

        EAGLContext* pCtx  = (EAGLContext*)pEgCtx;

        if(pCtx)

               [pCtx presentRenderbuffer:GL_RENDERBUFFER_OES];

}

 

es00_egl_iphone_init2.zip

 

 

12.3 OpenGL ES의 특징

EGL 객체들이 생성되었다면 OpenGL ES API를 사용해서 3D 장면을 구성할 수 있습니다. 간단히 ES API를 워크스테이션용 API의 축소판으로 생각해도 됩니다. 따라서 OpenGL에 경험이 있는 분들은 ES와의 차이를 확인해서 PC용 프로그램을 수정하면 됩니다.

 

몇 가지 OpenGL ESOpenGL와의 중요한 차이를 열거해보면 먼저 렌더링 방식의 간소화입니다. 예를 들어 Begin()/End()로 장면을 구성하는 OpenGL의 즉시 모드가 사라졌습니다. 3D 장면을 표현하려면 glDrawArrays() 또는 glDrawElements() 함수를 사용해야 합니다. 그리고 이 함수들은 정점 배열이 필요 하기 때문에 3D의 모든 데이터는 연속된 메모리 공간으로 구성해야 합니다.

다음으로 프리미티브가 좀 더 단순해졌습니다. 예를 들어 QUAD, POLYGON 등이 사라졌는데 이들 형태로 구성된 데이터가 있다면 인덱스 버퍼를 구성해서 glDrawElements() 함수로 구현할 수도 있습니다.

또한 파이프라인에서 취급되는 데이터 타입도 double형은 제거 되고 대신 ES는 고정 소수점(Fixed Float) API 함수가 추가되었습니다. 고정 소수점 API는 접미사 "x"를 사용합니다.

이 외에 텍스처는 2차원만 취급하고, 누적버퍼, 래스터 작업에서 사용된 함수들은 대부분 제거되었습니다. 기타 나머지 차이점들은 필요에 따라 OpenGL ES 에 대한 문서를 참고 하기 바랍니다.

 

1960년대 이후부터 그래픽스를 연구하는 많은 분들이 현실을 컴퓨터로 표현(흉내내기)하기 위해서 여러 적합한 모델을 연구하였고, 현재, 프리미티브라는 기본 요소를 결합해서 3D 가상 공간을 구성하는 방식을 사용하고 있고 프리미티브를 사용한 방식은 OpenGL, Direct3D 등 현재의 모든 3D Graphics에서 보편적으로 사용되고 있습니다.

프리미티브는 정점(Vertex)으로 구성됩니다. 정점은 공간의 좌표(위치)를 기본으로 렌더링에 필요한 정보들(빛에 대한 효과를 연산하는 법선 벡터, 색상, 텍스처 좌표 등)를 포함하고 있습니다.

 

OpenGL ES에서 프리미티브 데이터는 연속적인 스트림으로 구성되어야 합니다. OpenGL은 다중 스트림(Multi Data Stream)과 단일 스트림(Single Data Stream) 두 가지를 지원하는데 다중 스트림은 정점의 포맷((Vertex format)에 따라 각각의 스트림으로 구성하는 것입니다.

예를 들어 폴리곤이 위치와 색상으로 구성되어 있다면 다음과 같이 정점 정보들을 배열 등을 사용해서 기하학 물체를 표현할 수 있습니다.

 

// 위치에 대한 배열

GLfloat vertexArray[9] = {  -1.F,-1.F,0.F,     1.F,-1.F,0.F,     0.F,1.F,0.F };

// 색상에 대한 배열

GLfloat colorArray[12] = { 1.F,0.F,0.F,1.F0.F,1.F,0.F,1.F,   0.F,0.F,1.F,0.F};

// 렌더링

glDrawArrays(…);

 

만약 가변적인 메모리가 필요하다면 malloc() 함수를 사용해서 동적 메모리를 사용하거나 STL vector 컨테이너를 사용해도 됩니다.

 

다중 스트림과 다르게 단일 스트림은 스트림 하나에 정점의 모든 데이터를 포함시키는 방법입니다.

단일 스트림 방식을 Diect3D의 고정 파이프라인에서 주로 많이 사용되는 방법이기도 합니다. 차이점은 Direct3D는 위치, 법선 벡터, 색상, 텍스처 좌표 등 정해진 순서에 따라 스트림을 구성해야 하지만 OpenGL은 이러한 순서에 특별한 규약은 없습니다.

 

단일 스트림을 사용하는 경우에 정점 구조체를 이용하면 단일 스트림을 쉽게 다룰 수 있고 Direct3D와 호환성을 만들 수 있습니다.

만약 위치와 색상을 가진 단일 스트림을 구성하고자 한다면 다음과 같은 구조체를 먼저 선언합니다.

 

struct VtxD                   // 정점 구조체

{

        GLfloat x, y, z;       // 위치

        GLfloat r, g, b, a;    // 색상

};

 

다음은 이 구조체를 사용해서 간단하게 하나의 삼각형에 대한 프리미티브 데이터를 구성한 예입니다.

 

VtxD VtxArray[] =             // 정점 데이터

{

    -1.0F,-1.0F,0.0F,    1.0F,0.0F,0.0F,1.0F, // 0

     1.0F,-1.0F,0.0F,    0.0F,1.0F,0.0F,1.0F, // 1

     0.0F, 1.0F,0.0F,    0.0F,0.0F,1.0F,0.0F // 2

};

 

이렇게 구성한 정점 데이터 또는 프리미티브 데이터가 처리되기 위해서는 glEnableClientState() 함수를 사용해서 파이프라인의 스트림 사용을 활성화 시켜야 합니다.

 

// 렌더링 컨텍스트의 위치에 대한 정점 스트림 활성화

glEnableClientState(GL_VERTEX_ARRAY);

 

// 렌더링 컨텍스트의 색상에 대한 정점 스트림 활성화

glEnableClientState(GL_COLOR_ARRAY);

 

스트림을 활성화 하고 나면 남은 것은 파이프라인에 연결(Binding)하는 일입니다. 연결에서 주의할 것은 파이프라인에 연결은 정점을 구성하는 각각의 요소를 개별적으로 연결해야 한다는 것과 단일 스트림을 사용할 경우에 Offset 을 적용해야 합니다.

위치, 법선 벡터, 색상 버퍼, 텍스처 좌표에 대한 스트림에 대해서 각각 glVertexPointer(), glNormalPointer(), glColorPointer(), glTexCoordPointer() 함수 등을 사용해서 연결하는데 다음은 다중 스트림의 위치와 색상 버퍼를 연결하는 예입니다.

 

// 현재의 파이프라인에 정점 스트림 바인딩

glVertexPointer(3, GL_FLOAT, 0, vertexArray);

 

// 현재의 파이프라인에 색상 버퍼 바인딩

glColorPointer(4, GL_FLOAT, 0, colorArray);

 

스트림 연결 함수 glVertexPointer(), …, glTextCoordPointer() glNormalPointer들은 동일한 규격의 인수들을 사용합니다. 이들 함수의 첫 번째 인수는 차원과 같은 좌표의 수를 지정합니다.

예를 들어 정점 데이터의 위치가 x, y, z 로 작성되어 있다면 3을 적습니다. 색상 같은 경우 red, green, blue, alpha 4 개의 요소로 구성할 경우가 가장 많은데 이 때 glColorPointer() 함수의 인수는 4가 됩니다. 법선 벡터를 연결하는 glNormalPointer()함수는 다른 함수와 다르게 좌표의 수가 필요 없습니다. 왜냐하면 그래픽스에서 법선 벡터는 x, y, z 3차원으로 구성되기 때문입니다.

 

인수 리스트의 2 번째는 스트림의 형태입니다. 예를 들어 색상이 float 형이라면 GL_FLOAT으로 설정합니다. 부동 소수점 float 형이 가장 일반적이지만 임베디드 기기는 연산 속도를 위해서 부동 소수점 float형 보다 고정 소수점(fixed point)을 지원하기도 하는데 이 경우 GL_FIXED로 지정하면 됩니다.

 

인수 리스트 3 번째는 Offset 값으로 다중 스트림은 0으로 설정 합니다. 만약 단일 스트림을 사용할 경우에는 Offset 값을 조정해야 합니다. 정점 구조체를 사용하고 있다면 이 구조체의 크기를 Offset 값으로 설정하면 됩니다.

인수 리스트 4 번째는 데이터의 시작 위치 입니다. 단일 스트림의 경우 정확한 시작 위치를 지정하기 위해서 정점 데이터를 바이트 형 포인터를 캐스팅해서 시작 위치를 지정하는 것이 직관적입니다.

다음은 단일 스트림을 사용하는 경우에 Offset 과 시작 위치를 지정하는 예입니다.

 

INT   Stride = sizeof(VtxD);                  // 정점 스트림 데이터의 간격

char* pVtx   = (char*)VtxArray;                       // 정점 위치 버퍼 시작

glVertexPointer(3, GL_FLOAT, Stride, pVtx);

pVtx += sizeof(GLfloat)* 3;                   // 정점 색상 버퍼 시작 = GLfloat3

glColorPointer (4, GL_FLOAT, Stride, pVtx);

 

정점 데이터를 char형으로 캐스팅 한 이유는 'pVtx +=' 부분에서 1Byte씩 계산이 되도록 하기 위함입니다.

 

es01_triangle.zip

 

 

12.4 행렬과 변환(Matrix and Transform)

변환이란 간단히 말한다면 정점의 이동입니다. 컴퓨터 그래픽스는 실 세계(Real world)를 컴퓨터로 흉내 낸 가상세계를 만들기 위해서 3D 물체를 기하학적인 점, 선 삼각형 등의 프리미티브로 구성하고 affine 변환을 사용해서 3차원 공간에서 2차원 화면 좌표 위치로 정점의 위치를 변환합니다.

그래픽 프로세서는 입력 받은 3차원 데이터를 처리 단계가 일정한 파이프라인에서 변환을 수행하게 되는데 상대적인 모델 좌표들을 3D 세계의 절대 좌표로 변환하는 월드 변환(World Transform), 월드 좌표를 카메라 공간으로 변환하는 뷰 변환(Viewing Transform), 2차원 화면으로 변환하되 장치에 독립인 정규 변환 또는 투영 변환(Projection Transform), 정규 변환 좌표를 해당 장치 좌표로 변환하는 뷰포트 변환(Viewport Transform) 등이 있습니다.

파이프라인에서 정점은 행렬을 사용해서 변환하게 되는데 하나의 정점의 위치가 변환되는 과정은 크기, 회전, 평행 이동으로 나눌 수 있으며 이들은 행렬을 사용해서 표현이 가능합니다. 만약 정점이 3차원 좌표라면 크기, 회전, 이동을 4차원 동차 좌표(Homogeneous coordinate)로 구성한 다음 4x4 행렬 하나로 표현될 수 있습니다.

OpenGL은 월드 변환과 뷰 변환을 하나로 묶어서 월드-뷰 변환으로 처리하며, 이 변환에 대한 행렬을 만들고 파이프라인에 설정하면 카메라 효과를 만들게 됩니다.

게임에서 사용되는 투영 변환은 크게 투시(Perspective) 변환과 직교(Orthogonal) 변환 두 종류를 사용하는데 투시 변환은 3차원 가상세계를 표현하기 위해서 뷰 체적(viewing volume)을 사각뿔대 (Frustum)을 구성하는 것이며 직교 변환은 설계 도면처럼 원근감 없이 3D 물체가 같은 비율로 표현되는 것으로 2차원 게임에 주로 이용 됩니다.

 

이들 여러 변환 중에서 게임 개발자가 가장 많이 투자 하는 부분은 월드-뷰 변환입니다. 월드-뷰 변환과 투영 변환은 glMatirixMode() 함수 호출에서부터 시작됩니다. 이 함수는 파이프라인에 있는 월드-뷰 변환, 투영 변환, 그리고 텍스처 좌표 변환 세 가지에 대한 행렬을 지정하는 함수로써 월드-뷰 변환은 다음과 같이 GL_MODELVIEW 인수를 지정하면 됩니다.

 

glMatrixMode(GL_MODELVIEW);

 

파이프라인의 행렬 값을 직접 수정할 수 있는 함수 들이 존재 하는데 glScalef(), glRotatef(), glTranslatef() 함수들은 각각 크기, 회전, 평행 이동에 대한 값을 설정하는 함수 입니다. 그런데 이 함수들을 사용할 때 주의 할 것은 이들 함수들은 현재 설정되어 있는 행렬에 자신들의 행렬을 곱한다는 사실입니다.

예를 들어 glScalef() 함수를 사용하면 크기 변환 행렬을 만들어서 다음 수식과 같이 파이프라인의 행렬을 설정합니다.

 

활성화된 파이프라인의 행렬 *= 크기 변환 행렬[…];

 

<glScalef() 함수로 만들어진 행렬>

, , ,

 

< glScalef() 함수의 역할>

 

수식으로 알 수 있듯이 문제는 시간이 지날수록 이전 행렬에 누적해서 곱해진다는 것입니다. 행렬의 곱셈 결과가 누적이 안되게 하려면 파이프라인의 행렬을 초기화 해야 하는데 3D이 행렬은 덧셈 보다는 곱셈이 주로 사용되므로 행렬을 항등 행렬(또는 단위 행렬)로 초기화 합니다.

 

glLoadIdentity() 함수의 역할은 수식으로 표현하면 다음과 같이 활성화된 행렬을 항등 행렬로 설정하는 함수 입니다.

 

< glLoadIdentity() 함수의 역할>

 

glLoadIdentity() 함수를 사용해서 전체 장면을 그리기 전에 행렬을 초기화 하는 것이 좋습니다.

 

glClear(…);

glMatrixMode(GL_MODELVIEW);

glPushMatrix();

glLoadIdentity();

 

glScalef();

 

회전 변환에 대한 glRotatef() 함수는 glScalef()와 비슷하게 회전 변환 행렬을 만들어서 현재 활성화 되어 있는 행렬에 곱하는 함수 입니다.

 

활성화된 파이프라인의 행렬 *= 회전 변환 행렬[…];

 

< glRotatef() 함수로 만들어진 행렬>

,

 

< glRotatef() 함수의 역할>

 

이 함수도 올바르게 사용하려면 크기 변환과 같이 glLoadIentity() 함수를 호출해서 현재의 행렬을 항등 행렬로 만들어 놓고 사용합니다.

 

glMatrixMode(GL_MODELVIEW);

glLoadIdentity();

glRotatef();

 

glRotatef() 함수의 인수는 라디안(Radian)이 아닌 10진수(Degree) 회전각, 그리고 3차원 회전 축으로 구성되어 있어서 습니다. 예를 들어 x 축으로 30도 회전을 적용하려면 glRotatef(30, 1, 0, 0)으로 설정하면 됩니다.

 

glTranslatef() 함수는 평행 이동을 위한 행렬입니다. 이 함수는 평행 이동 변환 행렬을 만들어서 다음 수식과 같이 현재의 행렬에 곱셈을 합니다.

 

활성화된 파이프라인의 행렬 *= 이동 변환 행렬[…];

 

 

이 함수도 올바르게 사용하려면 크기 변환과 같이 glLoadIentity() 함수를 호출해서 현재의 행렬을 항등 행렬로 만들어 놓고 사용합니다.

 

glMatrixMode(GL_MODELVIEW);

glLoadIdentity();

glTranslatef();

 

애니메이션과 같이 3D 모델의 움직임을 위해서 크기 변환, 회전 변환, 이동 변환이 적용될 수 있습니다. 이 때 중요한 것은 이들의 적용 순서를 분명하게 하는 것이 중요한데 예들 들어서 다음과 같이 변환 함수 glScalef(), glRotatef(), glTranslatef() 가 적용하는 경우를 생각해 봅시다.

 

glTranslatef(0.0F, -15.0F, -100.0F);

glRotatef(30.0F, 0.0F,1.0F,0.0F);

glScalef(3.5F,0.1F,3.5F);

 

glDrawElements(…);

 

파이프 라인에서는 glDraw…() 함수에서부터 가까운 거리에 있는 함수부터 역순으로 glScalef(), glRotatef(), glTranslatef() 함수에 대한 변환을 적용합니다.

OpenGL은 변환에 대한 함수를 즉시 적용하지 않고 누적시켰다가 렌더링 처리를 요청하는 함수 Draw…() 함수가 호출될 때 역순으로 정점에 변환 함수들을 스택 자료 구조(stack data structure)처럼 적용시킵니다.

 

glScalef(), glRotatef(), glTranslatef() 함수는 각각 행렬을 만들어서 곱셈하는 것과 같습니다.

앞의 예를 4x4로 바꾸면 각각 다음과 같이 만들 수 있으며 변환된 정점의 위치는 이들을 순차적으로 곱셈하는 것과 같습니다.

 

(x’, y’, z’, 1) =  M_translate * M_rotation * M_scaling *(x,y,z,1)

 

OpenGL은 오른손 좌표계를 사용하기 때문에 행렬을 오른쪽에서 왼쪽으로 곱셈을 합니다. 하드웨어는 행 중심으로 처리하는 것이 빠르므로 실제로는 왼손 좌표계의 행렬 곱과 같은 모습이 됩니다.

 

(x’, y’, z’, 1) =  (x,y,z,1) * M_scaling * M_rotation * M_translate

 

이들의 곱셈 순서는 D3D와 동일한데 D3D도 수학 함수 처리는 같은 방식을 사용하기 때문입니다.

 

렌더링 오브젝트에 크기, 회전, 이동 변환을 각각 적용하는 것보다는 행렬들을 미리 연산하고 이것을 적용하는 것이 유리합니다. 예를 들어 렌더링 오브젝트의 폴리곤이 1000 정도 있다고 합시다.

한 정점과 변환 행렬의 산술 연산이 대략 20회 정도라 하고 크기, 회전, 이동 변환을 한다면 대략20 * 20 * 1000 회 정도가 소요되지만 크기, 회전, 이동 변환 행렬을 미리 연산하고 이 연산 결과를 폴리곤에 적용하면 산술 연산 횟수가 1/3로 줄어듭니다.

 

이 때 주의할 것은 행렬의 곱셈은 교환 법칙이 안되므로 행렬의 곱셈 순서는 매우 중요합니다.

변환 행렬들의 곱셈은 각자의 고유한 방법이 있을 수 있으나 게임에서 가장 보편적으로 사용하는 방법은 크기, 회전, 이동 변환 순서대로 곱셈하는 방법입니다. 이러한 이유는 3D 장면을 구성하는 렌더링 오브젝트는 지역좌표(Local Coordinate)계의 중심으로 구성하기 때문입니다.

3D Max, Maya 같은 그래픽 프로그램을 사용하는 Modeler들은 지역 좌표계를 가지고 폴리곤을 구성합니다. 이 때 지역 좌표계는 (0,0,0) 위치로 설정하는데 이 위치는 크기와 회전에 대해서 어떤 영향도 받지 않습니다. 이동 변환은 보통 월드 좌표계에서 오브젝트의 중심 위치와 대응되어야 하는데 지역 좌표계로 만들어진 렌더링 오브젝트에 월드 공간의 이동 변환을 먼저 적용하고 크기와 회전 변환을 적용하게 되면 중심 위치 또한 회전과 크기 변환을 받게 되기 때문에 이동 변환은 모든 변환의 마지막 단계에서 진행 됩니다.

 

다음은 행렬을 미리 연산해서 적용하는 예입니다.

 

glMatrixMode(GL_MODELVIEW);   // Model-view 행렬을 활성화

 

static int c =0;

++c;

float mtScl[16]={1,0,0,00,1,0,00,0,1,00,0,0,1,  };

float mtRot[16]={1,0,0,00,1,0,00,0,1,00,0,0,1,  };

float mtTrs[16]={1,0,0,00,1,0,00,0,1,00,0,0,1,  };

float mtWld[16]={1,0,0,00,1,0,00,0,1,00,0,0,1,  };

float mtWld2[16]={1,0,0,00,1,0,00,0,1,00,0,0,1,  };

 

//glTranslatef(0.0F, -15.0F, -100.0F);

//glRotatef(30.0F, 0.0F,1.0F,0.0F);

//glScalef(3.5F,0.1F,3.5F);

 

MatrixTranslate(&mtTrs[0], 0.F, -15.F, -100.F);         // 이동 행렬 구하기

MatrixRotationY(&mtRot[0], (float)(c));                 // 회전 행렬 구하기

MatrixScaling(&mtScl[0], 3.5F,0.1F,3.5F);       // 크기 변환 행렬 구하기

MatrixMultiple(&mtWld2[0], &mtScl[0], &mtRot[0]);  // 월드 행렬 = 크기 행렬 * 회전 행렬

MatrixMultiple(&mtWld2[0], &mtWld2[0], &mtTrs[0]); // 월드 행렬 *= 이동 행렬

glLoadMatrixf(&mtWld2[0]);                   // 계산된 월드 행렬을 파이프라인에 적용

 

glDrawElements(…);

 

es02_transform.zip

 

다시 강조한다면 하나의 오브젝트에 크기, 회전, 이동이 적용될 경우 이에 대한 행렬을 미리 계산에서 적용하는 것이 좋으며 최종 월드 행렬에 대한 각각의 변환 행렬의 곱셈 순서가 다음과 같음을 기억하기 바랍니다.

 

월드 행렬 = 크기 변환 행렬 * 회전 행렬 * 이동 행렬

 

생각해볼 문제) 태양을 중심으로 공전하는 달을 포함한 자전하는 9개의 태양계 행성을 시뮬레이션 할 때 달과 지구에 대한 월드 행렬의 곱셈을 어떻게 이루어질까요?

 

 

12.5 GLU 함수 만들기

-gluOrtho2D-

워크스테이션용 OpenGL 라이브러리에는 GLU(OpenGL Utility)와 같은 OpenGL를 좀 더 편리하게 사용하기 위한 함수들이 존재했었는데 아쉽게도 임베디드 라이브러리는 GLU가 없습니다. (안드로이드는 JAVA에 대한 GLU가 존재)

하지만 GLU OpenGL을 가지고 확장해서 만들었기 때문에 우리는 임베디드용 GLUgl…() 함수들을 사용해서 만들 수 있습니다. 가장 많이 사용되는 glu 함수 중에서 변환에 관련된 gluOrtho2D(), gluPerspective(), gluLootAt() 함수들을 구현해 보겠습니다.

 

gluOrtho2D() 함수는 glOrtho() 함수에서 Near = -1, Far = -1로 설정해서 다음과 같이 쉽게 구현할 수 있습니다.

 

void MyGluOrtho2D(float Left, float Right, float Bottom, float Top)

{

        float mt[4][4] ={0};

 

        glMatrixMode(GL_PROJECTION);

        glLoadIdentity();

        glOrtho(Left, Right, Bottom, Top, -1, 1);

        glMatrixMode(GL_MODELVIEW);

}

 

만약 glOrtho() 함수를 풀어서 구현하고자 한다면 glOrtho()함수로 얻어지는 행렬을 만들어야 합니다. glOrtho() 함수는 MSDN과 같은 문헌에는 다음 수식으로 알려져 있습니다.

 

 

하지만 실제로 glOrtho() 함수로 구현된 행렬 값은 왼손 좌표계의 값으로 구해지게 되는데 왼손좌표계에서 glOrtho()함수는 다음과 같이 정리 됩니다.

 

 

이것을 프로그램으로 구현하면 다음과 같습니다.

 

void MyGluOrtho(float Left, float Right, float Bottom, float Top,float Near, float Far)

{

        float mt[4][4] ={0};

 

        mt[0][0] = +2/(Right-Left);

        mt[3][0] = -(Right+Left)/(Right-Left);

        mt[1][1] = +2/(Top-Bottom);

        mt[3][1] = -(Top+Bottom)/(Top-Bottom);

        mt[2][2] = -2/(Far-Near);

        mt[3][2] = -(Far+Near)/(Far-Near);

        mt[3][3] = 1;

 

        glMatrixMode(GL_PROJECTION);

        glLoadMatrixf((const GLfloat*)&mt[0][0]);

        glMatrixMode(GL_MODELVIEW);

}

 

void MyGluOrtho2D(float Left, float Right, float Bottom, float Top)

{

        MyGluOrtho(Left, Right, Bottom, Top, -1, 1);

}

 

gluOrtho2D() 함수는 2D 게임을 만들 때 아주 유용하게 사용할 수 있습니다. 2D 게임의 화면은 좌 상단이 (0,0), 우 하단이 (화면 너비, 화면 높이)로 지정되는 경우가 많습니다.

이런 경우 gluOrtho2D(0, 화면 너비, 0, 화면 높이) 가 되는데 자신이 많은 gluOrtho()함수에 이 값을 넣었을 때 올바로 출력된다면 함수를 성공적으로 만든 것이라 할 수 있습니다.

 

es03_gluOrtho.zip

 

 

-gluPerspective-

OpenGL gluPerspective() 함수는 왼손 좌표계에서 다음과 같이 정리 됩니다.

 

 

이 공식을 glFrustum() 함수를 사용해서 적용할 수 있지만 이것을 직접 구현한다면 다음과 같이 만들 수 있습니다.

 

#include <math.h>

void MyGluPerspective(float* Out, float Fov, float Aspect, float Near, float Far)

{

        float   mtPrj[4][4] ={0};

        float   COT  = (float)tan(Fov/2. * 3.14159265358979/180.0 );

 

        COT = 1.0F/COT;

        mtPrj[0][0] =  COT/Aspect;

        mtPrj[1][1] =  COT;

        mtPrj[2][2] = -(Far+Near)/(Far-Near);

        mtPrj[3][2] = -2*Far*Near/(Far-Near);

        mtPrj[2][3] = -1;

        memcpy(Out, mtPrj, sizeof(float)*16);

}

 

어떤 임베디드기기 SDK cot() 함수 지원이 안될 수 있어서 여러분은 tan() 함수의 역수로 cot()를 사용하거나 cos()/sin() 함수로 계산할 때도 있습니다.

 

 

es03_gluPers.zip

 

 

-gluLookAt-

앞의 두 함수 gluOrtho(), gluPerspective() 함수는 파이프라인의 투영(Projection) 행렬을 설정하는 함수 입니다. 그런데 지금 구현한 gluLookAt()함수는 Model-View 행렬을 설정하는 함수로 간단히 카메라의 행렬을 설정하는 것으로 생각해도 좋습니다.

 

<OpenGL Camera>

 

D3DD3DXMatrixLookAtRH() 함수는 gluLookAt() 함수와 일치하기 때문에 구현 방법은 Direct3D document 문서를 가지고 만들어도 됩니다. 우리는 먼저 수식으로 알고리즘을 만들어 봅시다.

 

gluLookAt() 함수의 첫 번째 단계는 z축을 설정하는 것입니다.

다음으로 Up vector와 앞서 구한 z 축 벡터를 사용해서 카메라의 x 축을 설정합니다.

z, x, 축을 구했으니 z, x를 가지고 y 축을 계산합니다.

 

마지막 단계로 Model-View 행렬을 다음과 같이 구성합니다.

 

 

알고리즘은 간단한데 벡터의 정규화(Normalize)와 외적(Cross Product), 그리고 내적(Dot Product)가 있어서 이 부분 또한 구현해야 합니다.

정규화, 외적, 내적의 내용은 생략하고 3차원 벡터에 대해서 이들을 구현하면 다음과 같습니다.

 

void Vec3Normalize(float* v)

{

        float r = (float) sqrt( v[0]*v[0] + v[1]*v[1] + v[2]*v[2] );

        if (r == 0.0)

                return;

        r = 1.0F/r;

 

        v[0] *= r; v[1] *= r; v[2] *= r;

}

 

void Vec3Cross(float* Out, const float* v1, const float* v2)

{

        Out[0] = v1[1] * v2[2] - v1[2] * v2[1];

        Out[1] = v1[2] * v2[0] - v1[0] * v2[2];

        Out[2] = v1[0] * v2[1] - v1[1] * v2[0];

}

 

float Vec3Dot(const float* v1, const float* v2)

{

        return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];

}

 

이들 함수를 사용해서 gluLookAt() 함수 알고리즘을 수식으로 표현한 것을 코드로 옮긴다면 다음과 같이 구현됩니다.

 

void MyGluLookAt(float* Out, float EyeX, float EyeY, float EyeZ

        , float AtX, float AtY, float AtZ

        , float UpX, float UpY, float UpZ)

{

        float Eye[3]={EyeX, EyeY, EyeZ};

        float AxisX[3]={0}, AxisY[3]={UpX, UpY, UpZ}, AxisZ[3]={0};

        float m[4][4]={0};

 

        AxisZ[0] = EyeX - AtX;        // Z = Eye - LookAt

        AxisZ[1] = EyeY - AtY;

        AxisZ[2] = EyeZ - AtZ;

        Vec3Normalize(AxisZ);

 

        Vec3Cross(AxisX, AxisY, AxisZ);       // X = Y ^ Z

        Vec3Normalize(AxisX);

 

        Vec3Cross(AxisY, AxisZ, AxisX);       // Y = Z ^ X

 

        m[0][0]= AxisX[0];     m[0][1]= AxisY[0];     m[0][2]= AxisZ[0];

        m[1][0]= AxisX[1];     m[1][1]= AxisY[1];     m[1][2]= AxisZ[1];

        m[2][0]= AxisX[2];     m[2][1]= AxisY[2];     m[2][2]= AxisZ[2];

 

        m[3][0]= -Vec3Dot(Eye, AxisX);

        m[3][1]= -Vec3Dot(Eye, AxisY);

        m[3][2]= -Vec3Dot(Eye, AxisZ);

        m[3][3]= 1.0F;

 

        memcpy(Out, &m[0][0], sizeof(float)* 16);

}

 

es03_gluLookAt.zip

 

 

12.6 텍스처 사용(Texturing)

-Single Texturing-

OpenGL ES 2D 텍스처만 사용할 수 있고, 1.1은 최대 2개의 멀티 텍스처를 사용할 수 있습니다. 워크스테이션용 OpenGL의 텍스처 사용은 싱글 텍스처 처리 방식은 glEnable(), glBindTexture() 두 개 함수만 사용하면 되었고, 멀티텍스처(Multi-Texturing) 처리의 경우는 ARB 함수를 호출해서 사용했었습니다. ES에서 텍스처 처리는 ARB를 사용하는 것과 동일하고 API 함수가 지원됩니다.

 

ES에서 텍스처를 사용할 때 주의할 것은 먼저 이미지의 크기는 반드시 2의 승수로 되어 있어야 합니다. 간혹 2의 승수가 아니어도 동작하는 플랫폼 있지만 호환성을 위해서 이미지의 크기를 2의 승수로 맞추는 것이 좋습니다.

두 번째로 텍스처링에 대한 설정 함수의 호출 순서가 중요합니다. 이 순서가 맞지 않으면 PC Simulator에서 출력되더라도 대상 기기(Target Device)에서의 화면에는 제대로 출력되지 않은 경우가 많으므로 꼭 주의해야 합니다.

 

텍스처를 생성하는 방법은 OpenGL과 동일하게 색상 버퍼를 만들고 glGenTextures() 함수로 텍스처를 만든 다음 glBindTexture() 함수와 glTexImage2D() 함수를 사용해서 텍스처에 이미지 버퍼를 바인딩 하면 됩니다.

 

Int ImgW = 0, ImgH = 0, ImgC = 0, ImgF = 0;

unsigned char* pPxl = NULL;

 

// TGA 파일에서 이미지 버퍼 생성

Tga_FileRead(&pPxl, &ImgW, &ImgH, &ImgC, (char*)MEDIA_DIR"tex/bg256x256.tga");

glGenTextures(1, &m_TexID0);         // 텍스처 생성

 

glBindTexture(GL_TEXTURE_2D, m_TexID0); // 텍스처에 이미지 버퍼 바인딩

 

// 필터링, 어드레싱 설정

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, …);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, …);

// 텍스처의 이미지 명세

glTexImage2D(GL_TEXTURE_2D, 0, ImgF, ImgW, ImgH, 0, ImgF, GL_UNSIGNED_BYTE, pPxl);

 

free(pPxl);                           // 이미지 버퍼 지우기

 

단일 텍스처 처리 (Single Texturing)는 워크스테이션용 API와 동일하게 적용해도 되지만 다중 텍스처 처리(Multi-Texturing)을 고려해서 API 함수들을 사용한다면 다음과 같이 API 함수들을 호출해야 합니다.

 

// 0번 레이어(GL_TEXTURE0) 활성화

glActiveTexture(GL_TEXTURE0);

glClientActiveTexture(GL_TEXTURE0);

glEnable(GL_TEXTURE_2D);

 

// 텍스처 좌표와 바인딩

glBindTexture(GL_TEXTURE_2D, …);

glEnableClientState(GL_TEXTURE_COORD_ARRAY);

glTexCoordPointer(…);

 

glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); // 멀티텍스처 환경설정

glTexParameteri(…);                  // 텍스처 Filtering, Addressing 설정

glDrawArrays(GL_TRIANGLE_FAN, 0,4);   // 렌더링

 

glBindTexture(GL_TEXTURE_2D, 0);      // 텍스처 사용 해제

glDisable(GL_TEXTURE_2D);

 

<Single Texture: es04_tex_single.zip>

 

 

-Filtering, Addressing-

ES 1.1 버전은 간단한 3D Filtering은 워크스테이션용과 차이가 없기 때문에 생략하겠습니다. Addressing은 임베디드 플랫폼 마다 차이가 있는데 Clamp, Repeat는 대부분 지원되지만 Mirror의 경우 지원이 없는 경우도 있으니 먼저테스트를 해보기 바랍니다.

 

// Clamp

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

 

// Repeat

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

 

// Mirror

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT_OES);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT_OES);

 

  

<Addressing: Clamp, Repeat, Mirror. es04_tex_address.zip>

 

 

-Multi-Texturing-

텍스처 처리에서 어려운 부분이 멀티-텍스처입니다. 이것은 이론이 어려운 것이 아니라 API 함수들의 호출 순서가 중요하기 때문입니다.

앞서 Single Texturing에서 사용된 API 함수들의 호출 순서를 잘 기억하고 있다면 Multi-Texturing 도 쉽게 구현할 수 있습니다. 다음은 다중 텍스처 처리 예입니다.

 

// 0 Stage 텍스처 활성화, binging

glActiveTexture(GL_TEXTURE0);

glClientActiveTexture(GL_TEXTURE0);

glBindTexture (GL_TEXTURE_2D, m_TexID0);

glEnable(GL_TEXTURE_2D);

glTexCoordPointer(2, GL_FLOAT, 0, Tex);

 

// 0 Stage Blending 설정

glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

 

// 0 Stage의 텍스처 Filtering, Addressing 설정

glTexParameteri(GL_TEXTURE_2D, …);

 

// 1 Stage 텍스처 활성화, binging

glActiveTexture(GL_TEXTURE1);

glClientActiveTexture(GL_TEXTURE1);

glBindTexture (GL_TEXTURE_2D, m_TexID1);

glEnable(GL_TEXTURE_2D);

glTexCoordPointer(2, GL_FLOAT, 0, Tex);

 

// 1 Stage Blending 방법을 설정

glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

 

// 1 Stage 텍스처 Filtering, Addressing

glTexParameteri(GL_TEXTURE_2D, …);

 

glDrawArrays(LC_PT_TRIANGLE_FAN, 0, 4);              // Draw

 

glActiveTexture(GL_TEXTURE1);                // Multi-Texturing 해제

glBindTexture (GL_TEXTURE_2D, 0);

glDisable(GL_TEXTURE_2D);

 

glActiveTexture(GL_TEXTURE0);

glBindTexture (GL_TEXTURE_2D, 0);

glDisable(GL_TEXTURE_2D);

 

<Multi-Texturing: es04_tex_multi.zip>

 

glTexEnvi() 함수는 텍스처에서 Sampling한 픽셀을 적용하는 방법을 설정하는 함수로 Multi-Texturing 처리에서는 함수의 첫 번째와 두 번째 인수에 각각 GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE 값을 설정하고, 세 번째 인수는 GL_MODULATE, GL_DECAL, GL_BLEND 중에서 하나를 선택합니다. 디폴트 값은 GL_MODULATE 이며 이 상수는 두 색상을 [0, 1.0] 범위로 설정하고 최종 색상은 두 색상의 곱으로 결정하도록 합니다.

세 번째 인수는 플랫폼의 지원에 따라 GL_ADD, GL_COMBINE, GL_ADD_SIGNED, GL_INTERPOLATE, GL_SUBTRACT 등을 사용할 수도 있습니다.

 

 

12.7 고정 소수점(Fixed Point) 처리

고정 소수점 처리는 FPU(Floating Point Unit)가 없는 임베디드 시스템에서 실수 연산을 정수로 바꾸어 처리하는 방식으로 32비트를 나누어서 소수점 이하는 하위 16비트를 사용하고 상위 16비트는 정수와 부호를 사용하는 방법입니다.

현재 3D를 지원하는 임베디드 시스템은 FPU가 있어서 굳이 고정 소수점 처리를 사용하지 않아도 됩니다. 또한 고정 소수점을 사용하더라도 렌더링 속도의 차이는 거의 없고, (렌더링 부분에서 가장 많이 시간을 소모하는 부분은 Rasterize, Blending, 그리고 화면의 출력입니다.) 고정 소수점을 사용하게 되면 접미사가 "x"로 끝나는 API 함수를 사용해야 하고 행렬, 컨텐츠의 리소스 등을 전부 고정 소수점으로 바꾸어야 하기 때문에 현재는 많이 사용되지 않은 방식입니다.

 

부동 소수점(Floating Point)에서 고정 소수점으로 바꾸는 공식은 다음과 같습니다.

 

V_fixed_point = floor(V_floating_point * 65536)

 

65536 2 16승이므로 정수형의 경우 << 16으로 처리하면 되고 float, double형은 65536 값을 곱한 후에 나머지를 버립니다.

만약 고정 소수점을 사용하면 산술 연산을 조금 수정해야 합니다. 덧셈, 뺄셈은 변함이 없으나 곱셈의 경우 두 고정 소수점은 65536 값을 이미 곱한 결과이기 때문에 65536으로 나누어야 하고 나눗셈의 경우는 65536을 다시 곱해야 합니다.

 

예를 들어 부동 소수점 v1, v2가 있고, 이 두 변수의 곱셈의 결과를 v3 합시다. 그리고 v1, v2, v3의 고정 소수점을 각각 v1', v2', v3' 라 가정하고 이들을 풀어 봅시다.

 

v3를 고정 소수점으로 바꾸면 65536을 곱해야 합니다.

 

v3' = v3 * 65536 = v1 * v2 * 65536 이 되고 이를 완전한 고정소수점의 곱으로 바꾼다면

v3' = v1 * v2 * 65536 * 65536 / 65536

    = (v1 * 65536) * (v2 * 65536) / 65536

    = v1' * v2' / 65536

 

고정 소수점의 곱은 65536으로 나누게 됩니다. 나눗셈도 마찬가지로 풀어보면

 

v3' = v3 * 65536 = (v1 / v2) * 65536 이 되고 이를 완전한 고정소수점의 곱으로 바꾼다면

v3' = ( v1/v2 * (65536/65536) ) * 65536

    = ( (v1 * 65536) / (v2 * 65536) ) * 65536

    = v1' / v2' * 65536

 

이 되어 고정 소수점의 나눗셈은 나눗셈 결과에 65536 값을 곱해야 합니다.

 

int LCXFixed(int v)                  { return int(v * 65536.0F); }

int LCXFixedMul(int v1, int v2){ return (v1 * v2) / 65536; }

int LCXFixedMiv(int v1, int v2){ return (v1 * 65536)/v2 ); }

 

65536 2 16승이므로 곱셈과 나눗셈을 쉬프트(shift) 연산으로 바꿀 수 있는데 이런 경우 32비트 int 형보다 좀더 큰 64비트 int형으로 casting 해서 사용하는 것이 좋습니다.



Copyright (c) 2004 3dapi.com All rights reserved.

Creative Commons License