Monday, March 10, 2014

OpenGL ES for Android (1)

안드로이드에서 OpenGL ES 2.0을 활용한 어플리케이션 개발
Open GL ES 2 for Android, A Quick Start Guide, Kevin Brothaler, 2013에 기반하여 첨삭 및 자의적 해석으로 작성된 문서입니다.



우리가 작성하려는 게 안드로이드에서 동작하는 OpenGL 어플리케이션이니 당연히 안드로이드 개발을 위한 기본 준비는 되어 있어야 하죠. 기본적인 설치 방법 등은 구글링만 해봐도 수두룩 하게 나오니 별도로 설명하지 않겠습니다.이 강좌의 컨텐츠들은  아래의 것들을 사용해서 작성되었습니다.

  • Mac OS X (Maverick)
  • JDK 6 
  • Eclipse IDE
  • OpenGL ES 2.0을 지원하는 에뮬레이터나 스마트폰, 테블릿이 필요한데, 이 블로그의 다른 강좌에서 처럼 Genymotion을 에뮬레이터로 사용할께요.(안드로이드에서 제공하는 에뮬레이터를 사용하실 거라면 AVD를 생성하는 과정에서 Use Host GPU를 꼭 체크하세요)



그럼 첫 번째 프로젝트를 생성해 보겠습니다

프로젝트 생성의 첫 화면인데요. OpenGL ES 2.0을 완벽하게 지원하는 최소 버전이 2.3.3(Gingerbread) 입니다. 그러니, Minimum Required SDK를 API 10으로 지정해 주세요.



어짜피 나중에 아이콘은 별도로 만들어 줄테니, Create custom launcher icon은 체크를 풀어주시고요.







여기까지 하시고 Finish를 눌러 주시면 안드로이드 프로젝트가 생성되겠죠?



코드의 작성 : GLSurfaceView를 이용한 OpenGL 초기화 과정

GLSurfaceView클래스는 OpenGL을 초기화 할 때 껄끄러운 부분들을 처리해 줍니다. 디스플레이를 설정하고 렌더링하는 등의 작업을 백그라운드 스레드에서 처리해주죠. 렌더링 같은 경우는 디스플레이의 정해진 영역에서 이루어지는데, 이 부분을 Surface (경우 따라선 viewport)이라고 부릅니다.

또한, GLSurfaceView 클래스는 표준 안드로이드 액티비티의 생명 주기를 처리하는 걸 아주 쉽게 해주기도 합니다. 안드로이드에서 액티비티는 생성되고 소멸되고, 일시 먼춤도 하고 재시작하기도 하고 그러잖아요?  이 생명주기에 따라 여러분은 액티비티가 멈추게 되면 OpenGL 리소스를 적절하게 놔주어야 하는데, GLSurfaceView는 이것을 처리하기 위한 헬퍼 메소드를 제공합니다.

그럼 방금 만든 프로젝트에서 FirstOpenGLProjectActivity.java 파일을 열어서 아래와 같이 작성해 줍시다.


GLSurfaceView 인스턴스 생성

1. 우선 멤버 변수 두 개를 추가해 줍니다.
public class FirstOpenGLProjectActivity extends Activity{
   private GLSurfaceView glSurfaceView;
   private boolean rendererset = false;

GLSurfaceView 객체를 위한 레퍼런스와 , GLSurfaceView가 유효한 상태인지 아닌지를 판가름하기 위한 부울 변수네요.

2. setContentView()메소드 호출부분을 지우고, 아래 코드를 집어 넣으세요
@Override
public void onCreate(Bundle savedInstanceState){
   super.onCreate(savedInstanceState);
   glSurfaceView = new GLSurfaceView(this);



OpenGL ES 2.0을 지원하는 시스템인지 체크, 렌더링이 이루어질 Surface 설정

3. 아래의 윗부분은 시스템이 OpenGL ES 2.0을 지원하는 지를 확인하는 코드입니다. ActivityManager에 대한 레퍼런스를 획득해서 디바이스 설정 정보를 확인하고, 그 다음에 reqGlEsVersion에 접근해서 지원하는 OpenGL ES 버전을 다시 확인하는 거죠.(GPU 에뮬레이션서는 이게 버그가 있어서 제대로 동작하지 않는다고 하네요. 전 안해봐서 모르겠습니다. Genymotion에서는 아주 잘 돌아가거든요)

그리고, 그 아래  if문에서는 surface에 렌더링 하는 것을 설정해 줍니다.( 디바이스가 OpenGL ES 2.0을 지원하면 setEGLContextClientVerions(2)를 호출해서 surface view를 설정합니다. 그 다음 setRenderer()를 호출해서 (아직은 작성하지 않았지만 이제 작성할) Renderer 클래스의 객체를 넘겨주죠. 그리고 나서 rendererSet를 true로 변경시켜주네요. renderer는 GLSurfaceView가 surface가 생성되거나 변경될 때, 새로운 프레임을 그려야 할때 마다 호출해서 사용합니다)

그리고 나서 setContentView()를 호출하면서 glSurfaceView를 넘겨주네요.
        final ActivityManager activityManager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
        final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
        final boolean supportsEs2 = configurationInfo.reqGlEsVersion >= 0x20000;
        
        if(supportsEs2){
           glSurfaceView.setEGLContextClientVersion(2);
           glSurfaceView.setRenderer(new FirstOpenGLProjectRenderer());
           rendererSet = true;
        }else{
           Toast.makeText(this, "This device does not support OpenGL ES 2.0.", Toast.LENGTH_LONG).show();
        }
        
        setContentView(glSurfaceView);



안드로이드 액티비티의 생명주기에 맞춰 처리해 주기

4. 그리고, onPause(), onResume()도 아래처럼 수정해 줍니다. 액티비티가 일시멈춤 되거나 다시 시작할 때 surface도 거기에 맞게 동작해야할테니까요.

   @Override
   protected void onPause() {
      super.onPause();
      if(rendererSet){
         glSurfaceView.onPause();
      }
   }


   @Override
   protected void onResume() {
      super.onResume();
      if(rendererSet){
         glSurfaceView.onResume();
      }
   }




완성된 코드 : FirstOpenGLProjectActivity.java

public class FirstOpenGLProjectActivity extends Activity {
   
   private GLSurfaceView glSurfaceView;
   private boolean rendererSet = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        glSurfaceView = new GLSurfaceView(this);
        
        final ActivityManager activityManager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
        final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
        final boolean supportsEs2 = configurationInfo.reqGlEsVersion >= 0x20000;
        
        if(supportsEs2){
           glSurfaceView.setEGLContextClientVersion(2);
           glSurfaceView.setRenderer(new FirstOpenGLProjectRenderer());
           rendererSet = true;
        }else{
           Toast.makeText(this, "This device does not support OpenGL ES 2.0.", Toast.LENGTH_LONG).show();
        }
        
        setContentView(glSurfaceView);
    }


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.first_open_glproject, menu);
        return true;
    }


   @Override
   protected void onPause() {
      super.onPause();
      if(rendererSet){
         glSurfaceView.onPause();
      }
   }


   @Override
   protected void onResume() {
      super.onResume();
      if(rendererSet){
         glSurfaceView.onResume();
      }
   }
    
}




Renderer 클래스의 작성 : FirstOpenGLProjectRenderer

이 클래스는 Renderer 인터페이스를 구현해서 작성합니다. 우선 Renderer인터페이스에 대해 살펴볼까요? (API 문서를 꼭 살펴보도록 하세요.  http://developer.android.com/reference/android/opengl/GLSurfaceView.Renderer.html)
Renderer 인터페이스에는 세 개의 메소드가 선언되어 있습니다


OnDrawFrame(GL10 glUnused)
프레임을 그려야 할때가 오면 GLSurfaceView는 이 메소드를 호출합니다. 여기서 '뭔가를 그리는 작업'이 이루어저야 합니다. 그게 화면을 클리어하는 일 정도 뿐이라 할지라도 말이죠. 이 메소드가 리턴되어진 후에 렌더링 버퍼가 스와핑되고 화면에 뿌려지기 때문에 우리가 아무것도 그리지 않는다면  화면이 깜박거리(flickering effect)는 일이 발생할 수 있습니다.

onSurfaceChanged(GL10 gl, int width, int height)
Surface가생성된 후에나, 크기가 변화하였을 때 GLSurfaceView는 이 메소드를 호출합니다.크기가 변화했다는 말은, 화면을 가로(LANDSCAPE)로 두다 세로(PORTRAIT)로 변경했을 때 혹은 그 반대가 되겠네요.

onSurfaceCreate(GL10 glUnused, EGLConfig config)
Surface가 생성될 때 GLSurfaceView에 의해 호출됩니다. 어플리케이션이 실행되면 가장 먼저 어떤 작업이 이루어지는 메소드인데요. 디바이스가 휴면상태에서 깨어날 때도, 사용자가 back 버튼을 눌러 어플리케이션으로 다시 돌아올 때에도 호출이 되죠. 어플리케이션이 종료되기 전까진 여러 상황에서 다시, 여러 번 호출 될 수 있다는 얘기입니다.


GL10이라는 사용되지 않는 변수가 보이죠? 이건 OpenGL ES 1.0 API에서의 흔적인데요. 1.0 렌더러를 만든가면 사용하겠지만, 지금 우리가 작성하는건 2.0이니 사용하지 않을 거구요. 대신 GLE20 클래스의 static method를 사용할 것입니다.
Renderer의 메소드들은 별도의 스레드에서 호출이 됩니다. GLSurfaceView는 기본적으로 지속적인 렌더링을 하도록 되어 있는데요. 보통은 1초당 몇 회를 화면에 다시 그릴 것인지, 정해진 재생률에 맞게 그려지게 됩니다. 하지만, GLSurfaceView의 setRenderMode()메소드를 이용해서(GLSurfaceView.RENDERMODE_WHEN_DIRTY를 인자로 사용), 요청이 있을 경우에만 렌더링 하게 할 수도 있죠.

안드로이드 GLSurfaceView는 백그라운드 스레드에서 렌더링을 하기 때문에, OpenGL을 이용할 경우에도 렌더링 스레드에서만 작업을 수행하고, 안드로이드 UI에 관련된 것은 메인 스레드에서 이루어지도록 관리해야 합니다.  이때,  백그라운드 렌더링 스레드에 Runnable 객체를 보내어 처리되도록 하기 위해서는 GLSurfaceView의 queueEvent(Runnable r)를 호출해 처리해 주면 되고. 메인 스레드에 이벤트를 보내 처리하게 하려면, 액티비티의 runOnUIThread(Runnable r)을 이용하면 됩니다.
그럼 Renderer 클래스를 작성해 볼까요?



코드 작성 : FirstOpenGLProjectRenderer.java

import static android.opengl.GLES20.*;
import static android.opengl.GLUtils.*;
import static android.opengl.Matrix.*;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;

public class FirstOpenGLProjectRenderer implements Renderer {

   @Override
   public void onDrawFrame(GL10 arg0) {
      glClear(GL_COLOR_BUFFER_BIT);
   }

   @Override
   public void onSurfaceChanged(GL10 glUnused, int width, int height) {
      glViewport(0, 0, width, height);
   }

   @Override
   public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
      glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
   }

}

public void onSurfaceCreated(...)
스크린에 뿌려질 색깔을 지정하고 있습니다. Red, Green, Blue, Alpah 의 네 가지 값으로 색깔을 정하고 있으니 여기서는 빨간색이 되겠네요.
public void onSurfaceChanged(...)
surface 전체를 차지하도록 viewport size를 정하고 있습니다
public void onDrawFrame(...)
화면을 싹 닦아내고 이전에 glClearColor()를 통해 정해진 색깔로 화면을 채웁니다.
static import위 코드에 보시면 import static 구문을 쓰고 있어요. 위에 쓰인 메소드들 glClear()나 거기에 넘겨지는 GL_COLOR_BUFFER_BIT같은 상수처럼, OpenGL을 이용하다보면 그 같은 static 메소드나 상수를 자주 쓰게 되거든요? 근데,  일일이 클래스명과 함께 쓰려면 여간 귀찮아지는 게 아니예요. 그래서 부득이 하게 static import을 사용합니다.
그런데, 안타깝게도 이클립스는 기본 설정으로는 static 메소드에 대한 '타입 검색, 자동 import, 완성'을 해주질 않아요. 기본 설정에 우리가 추가해줘야 하죠. 
Window->Preferences, Java->Editor->Content Assist->Favorites 메뉴를 따라가셔서
anroid.opengl.GLES20, android.opengl.GLutils, android.opengl.Matrix 이 세 개의 클래스를 추가해주세요.


첫 번째 OpenGL 프로젝트의 실행

다른 안드로이드 프로젝트와 마찬가지로 실행시켜보겠습니다.  그림에서 보듯 이 어플리케이션은 Genymoiton을 이용해 실행시켜본 화면입니다.



그런데, 언제쯤 Dalvim VM은 Code Hot Swap을 지원하게 될까요?





No comments:

Post a Comment