IT_Programming/OpenGL

[펌] GLSurfaceView 분석

JJun ™ 2008. 12. 29. 09:28



 출처

 : http://anddev.tistory.com/15

 : http://anddev.tistory.com/18



이번엔 안드로이드의 3D 샘플과 함께 제공되는 GLSurfaceView의 구현에 대해 분석해보겠습니다.

이 클래스는 OpenGL API를 사용하고 있기 때문에 OpenGL의 기본적인 내용은 알아야 합니다.

분석의 방법은...
제가 개인적으로 쓰는 방법입니다만...
보통 객체지향 언어는 캡슐화가 용이하다는 장점이 있는 반면
그에 대한 반대 급부로 코드의 해독이 난해해질 수 있다는 단점이 존재합니다.

따라서
분석 대상이 되는 코드에 친숙하지 않은 부분이 포함되어 있을 경우
캡슐화 되어 있는 내용을 풀어 Verbose하게 표현해보는 겁니다. 
그리고 다시 역으로 거슬러 올라가면서
왜 이런 패턴을 적용하게 되었는지 원저자의 의도를 유추해내는 것이죠.

그래서 우선 GLSurfaceView 클래스가 하게 되는 일들을 시퀀셜하게 표현할 수 있도록 
다음과 같이 풀어보았습니다.


private static class SampleView extends SurfaceView implements SurfaceHolder.Callback {

public static final String TAG = SampleView.class.getSimpleName();


private EGL10 mEgl;

private EGLDisplay mEglDisplay;

private EGLConfig mEglConfig;

private EGLContext mEglContext;

private EGLSurface mEglSurface;


public SampleView(Context context) {

super(context);

SurfaceHolder holder = getHolder();

holder.addCallback(this);


initOpenGL();

}


private void initOpenGL() {

mEgl = (EGL10) EGLContext.getEGL();

mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);


int[] version = new int[2];

mEgl.eglInitialize(mEglDisplay, version);


int[] configSpec = {

EGL10.EGL_RED_SIZE,      8,

EGL10.EGL_GREEN_SIZE,    8,

EGL10.EGL_BLUE_SIZE,     8,

EGL10.EGL_ALPHA_SIZE,    8,

EGL10.EGL_DEPTH_SIZE,   16,

EGL10.EGL_NONE

};


int[] num_config = new int[1];

EGLConfig[] configs = new EGLConfig[1];

mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1,

num_config);

mEglConfig = configs[0];


mEglContext = mEgl.eglCreateContext(mEglDisplay, configs[0],

EGL10.EGL_NO_CONTEXTnull);

if (mEglContext == null | mEgl.eglGetError() != EGL10.EGL_SUCCESS)

{

Log.w(TAG"Failed to get EGL context");

}

}


public void surfaceCreated(final SurfaceHolder holder) {

Log.v(TAG"surfaceCreated");


mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay,

mEglConfig, holder, null);

mEgl.eglMakeCurrent(mEglDisplaymEglSurfacemEglSurface,

mEglContext);


final GL10 gl = (GL10) mEglContext.getGL();

Log.v(TAG"GL10: "+gl);


//뷰포트 설정

Rect size = getHolder().getSurfaceFrame();

gl.glViewport(0, 0, size.width(), size.height());


//좌표계 초기화

gl.glMatrixMode(GL10.GL_PROJECTION);

gl.glLoadIdentity();


gl.glOrthof(-100, 100, -100, 100, 1, -1);


//배경화면 초기화

gl.glClearColor(0.5f, 0.5f, 0.5f, 1f);

gl.glClearDepthf(1.0f);

gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

Log.v(TAG"clear color");


//모델링

gl.glMatrixMode(GL10.GL_MODELVIEW);

gl.glLoadIdentity();


draw(gl);


mEgl.eglSwapBuffers(mEglDisplaymEglSurface);

Log.v(TAG"swap buffer");

};


private void draw(GL10 gl) {

//여기에서 실제 그리기 작업이 시작됩니다.

}


public void surfaceChanged(SurfaceHolder holder, int format, int width,

int height) {

Log.v(TAG"surfaceChanged");

GL10 gl = ((GL10) mEglContext.getGL());


gl.glViewport(0, 0, width, height);

}


public void surfaceDestroyed(SurfaceHolder holder) {

Log.v(TAG"surfaceDestroyed");

mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACEmEglContext);

mEgl.eglDestroyContext(mEglDisplaymEglContext);

mEgl.eglTerminate(mEglDisplay);

mEglSurface = null;

mEglContext = null;

mEglDisplay = null;

}

}


다음 글에서 실제 GLSurfaceView의 디자인에 대해 살펴보도록 하겠습니다. 
위의 내용들이 어떻게 클래스로 분리되어 표현되는지 눈여겨 보시면 도움이 될겁니다. 





GLSurfaceView는 SurfaceView의 하위 클래스이며 내부적으로 OpenGL 요청을 처리하기 위한 기능을 담고 있습니다.
GLSurfaceView에는 다음과 같이 1개의 인터페이스와 2개의 클래스를 내부에 가지고 있습니다.

  • 인터페이스
    • Renderer: 각 Frame에 그림을 그립니다.
  • 클래스
    • GLThread: Renderer를 주기적으로 호출합니다.
    • EGLHelper: EGL 설정을 담당합니다.

하나의 Activity에 하나의 GLSurfaceView가 존재하는 상황을 가정하겠습니다. 해당 Activity가 생성될 때 다음그림과 같은 과정이 수행됩니다.
푸른 색의 화살표는 Main 쓰레드를 뜻합니다.




  1. Activity의 onCreate 콜백 메소드가 호출됩니다.
  2. OnCreate메소드는 GLSurfaceView와 Renderer 객체를 생성하고 GLSurfaceView 객체에 할당합니다.
  3. GLSurfaceView는 GLThread를 생성하고 Renderer객체를 할당합니다.
  4. GLThread를 start 합니다.
  5. 생성된 GLSurfaceView를 Activity의 컨텐트 뷰로 설정합니다. 

이제 GLThread는 별도의 쓰레드로 동작하게 됩니다. GLThread는 루프를 돌며 주기적으로 Renderer에게 화면 그리기를 요청합니다.  
구체적인 동작은 다음과 같습니다.

GLThread는 Thread 클래스의 하위 클래스입니다. Thread클래스의 start메소드를 호출하면 새로운 쓰레드가 생성된 후 run메소드에 정의된 동작들이 수행됩니다.
아래의 그림은 쓰레드가 생성된 이후의 과정을 보여줍니다. 주황색 화살표는 GLThread를 뜻합니다.




  1. GLThread는 EGLHelper를 생성합니다.
  2. GLThread는 EglContext생성을 위한 Configuration을 Renderer에게 질의합니다.
  3. Renderer의 Configuration에 상응하는 EglContext와 EglDisplay가 생성됩니다.
  4. 이벤트 큐를 확인한 후 아무것도 없으면 wait 상태에 들어갑니다.
    (반드시 wait가 호출되는 것은 아니며 내부 상태 변수에 따라 동작이 변할 수 있으나 여기에서는 상황을 단순화하여 표현하기 위해 이렇게 표현함)


다음 그림은 Surface가 생성된 후 SurfaceHolder.Callback 인터페이스의 surfaceCreated가 호출된 경우를 보여줍니다.




  1. GLThread의 surfaceCreated 메소드를 호출. 메소드 내부에서 GLThread를 notify합니다.
  2. notify된 GLThread가 EGLHelper에게 createSurface를 호출합니다.
  3. Surface가 생성되었음을 Renderer에게 알립니다. 
  4. Renderer에게 drawFrame을 요청합니다.
  5. 다 그린후 버퍼를 스왑하여 화면에 표시합니다(더블 버퍼링).

GLThread는 내부적으로 EventQueue를 가지고 있습니다. 동작 방식을 우선 살펴본 후 EventQueue가 왜 존재하는지를 고려해보겠습니다.
아래의 그림은 사용자가 스크린을 터치하여 해당 이벤트가 GLSurfaceView에 잡힌 이후의 상황을 보여줍니다.





  1. Touch 이벤트에 대응되는 동작을 기술한 Runnable 객체를 이벤트 큐에 삽입합니다.
  2. GL 쓰레드는 이벤트 큐를 검사하고 이벤트가 존재할 경우 해당 이벤트를 실행합니다.
  3. 이후 여러 과정을 거친 후, Renderer의 drawFrame을 호출합니다. 

GLThread의 EventQueue는 왜 존재할까요? 이것 역시 일반적인 UI모델을 흉내낸 것입니다. Eclipse나 Swing의 UI 툴킷은 Single Threaded 모델을 채택하고 있습니다.
UI의 무분별한 갱신을 막고 일관성있는 화면 갱신 메카니즘을 제공하기 위한 것입니다. 그리고 사용자의 비동기적인 요구에 대응하기 위해 이벤트 큐를 유지하며
사용자의 요구는 이벤트 큐에 일단 저장되었다가 적절한 시점에 일괄적으로 처리합니다. 마찬가지로 GLSurfaceView도 화면 갱신을 위해서 단일 GLThread를 갖고
있으며 화면에 대한 추가적인 요구는 eventQueue를 이용하게 되어 있습니다. 그리고 또 하나의 특징은 푸른색의 화살표는 메인 쓰레드로서 여기에서 롱타임 연산이
발생할 경우 전체 UI의 멈춤 현상이 발생하게 됩니다. 따라서 롱타임 연산은 Runnable인터페이스로 감싸서 이벤트 큐에 등록하고 GLThread는 적절한 시점에
해당 연산을 수행하도록 함으로 전체 UI의 응답성을 훼손하지 않게 됩니다.


단점은 현재의 구현에서 Frame rate을 적절히 제어하는 코드가 없다는 점입니다. 이런 상황에서는 빠른 디바이스에서는 빠른 애니메이션을, 느린 디바이스에서는
느린 애니메이션을 보여주게 됩니다. 따라서 디바이스에 상관없이 일정한 Frame rate을 보여주게 하는 기능을 추가해야 할 것 같습니다.
이 부분에 대한 내용은 나중에 다시 다루기로 하지요.