SSISO Community

시소당

안드로이드 오픈지엘(OpenGL ES2.0)의 기본

구글에서 제공하는 openGL ES2.0의 샘플 코드를 설명해본다.
기초적인 안드로이드 앱 개발지식은 있고 3D는 처음이라고 간주하겠다.

이 포스트에서 참고한 소스는 안드로이드의 오픈지엘 예제 코드(JAVA)이다.
http://developer.android.com/training/graphics/opengl/index.html



예제 앱은 매우 단순하다. 손가락을 스크린에 터치상태로 움직이면 노란색 삼각형이 회전한다.
3D느낌이 없는 예제라 아쉽지만 가장 기본적인 도형 그리기라 좋은 예제가 되겠다.

소스를 보니 5개의 자바 클래스가 보인다. 이 예제는 순전히 자바로만 작성돼있다. openGL은 원래 API가 C로 되어있지만 안드로이드에서 제공하는 openGL은 자바로 한번 감싸여있다고 보면 되겠다. (물론 NDK를 이용해 C로 개발가능하다.) 구글에선 '별 차이 없으니 자바로 된거 써요’라고 한다. 하지만 C로 만들었을 때 다른 플랫폼에서 재활용될 수 있어 C가 익숙하다면 C로 작성하는게 좋을것 같다. 여기선 편의상 자바를 이용한다.

먼저 AndroidManifest.xml을 보자. openGL ES 2.0을 학습하는 입장에서 주목을 끄는 것은 아래의 2개의 항목일 것이다.

<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17" />

최하 버전이 8임을 의미한다. 8은 openGL ES 2.0의 최소 지원버전이 되겠다.

<uses-feature android:glEsVersion="0x00020000" android:required="true" />
사용하는 OpenGL ES의 버전을 명기했다. 0x00020000 즉 2.0을 의미한다. 이 값을 이용해 구글플레이에서는 2.0을 지원하지 않는 기기는 아예 검색에 노출되지 않을 것이다. OpenGL 1.0과 2.0은 큰 차이를 보인다. 하지만 OpenGL 2.0과 모바일용인 OpenGL ES 2.0은 큰 차이가 없다. API모습이 다르기 보다는 모바일 성능을 고려한 버퍼 사용, 크기 등이 다를 뿐이다.

이제 자바소스를 보자.
  1. MyGLRenderer.java
  2. MyGLSurfaceView.java
  3. OpenGLES20Activity.java
  4. Square.java
  5. Triangle.java

MyGLRenderer.java
랜더링 역할을 수행한다. 보통 랜더링이란 '주어진 데이터를 눈에 보이게 그리는것을 의미한다.

MyGLSurfaceView.java
GLSurfaceView를 상속 받았다. 도화지 역할을한다. 안드로이드에서 openGL로 그림 그리고 싶을 때 이 객체를 이용하게된다. 뿐만 아니라 기본적인 View의 역할도한다. 즉 유저 인터렉션을 받는 기능도 있다.

OpenGLES20Activity.java
20은 OpenGL ES 2.0을 의미한다. 그냥 액티비티를 상속받은 액티비티다. 위의 MyGLSurfaceView의 인스턴스를 만들어 액티비티의 뷰로 장착한다.

Square.java
이름에서도 알 수 있듯이 사각형이다. 위의 그림에서 파란색 사각형이다. 보통 어떤 샘플은 랜더러에서 그림그리기를하지만 이 샘플은 Square에게 위임을 했다. 랜더러형님 왈 "사각형아. 사각형 그리는건 너가 잘 아니깐 너가 알아서 그려봐”
그래서 이 객체 안에 사각형의 정점좌표 데이터도 들어있고 어떻게 그릴지에 대한 방식도 정의되어있다. 심지어 glsl 코드도 있다. 형님이 얘를 매우 신뢰하는 상황. 고로 이 예제의 핵심은 얘가 되겠다.

(GLSL이란 GPU에 주입되어 돌아갈 프로그램을 의미한다. GPU는 자바나 C코드를 못 알아 먹는다. CPU용이니깐. 그래서 GPU용 어셈블리어로 짠다..라고 하면 옛날 이야기이고 C와 비슷하게 생긴 코드로 짠다. C코드가 CPU가 이해할 수 있는 어셈으로 컴파일 되는것처럼 GLSL은 GPU용 명령어들로 구성된 바이트로 컴파일된다.)

이 glsl코드는 보통 별도의 파일로 작성되지만 여기선 간단한 샘플이라 java코드 안에  들어있다. glsl이 별도의 파일로 존재하는 이유는 glsl만 간단히 교체할 수 있는 구조가 필요한 이유와 같겠다. 작업자가 다르다던가, 외부에서 다운로드해서 동적으로 교체할 수 있게 하던가, 어떤 툴에서 내뱉는 glsl코드를 Android Studio를 열지 않고 쉽게 교체할 수 있게 하고 싶다던가 등등.

Triangle.java
위의 Square.java와 같다. 삼각형 정보와 로직을 갖고있다.


1. OpenGLES20Activity

이제 코드를 살펴보겠다. 먼저 인스턴스가 가장 먼저 생성되는 OpenGLES20Activity를 보자. 열어보니 이 클래스는 볼게 없어서 진도를 빨리 뺄 수 있겠다. 코드는 다음과 같다.

package com.example.android.opengl;
import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
public class OpenGLES20Activity extends Activity {
    private GLSurfaceView mGLView;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Create a GLSurfaceView instance and set it
        // as the ContentView for this Activity
        mGLView = new MyGLSurfaceView(this);
        setContentView(mGLView);
    }
    @Override
    protected void onPause() {
        super.onPause();
        // The following call pauses the rendering thread.
        // If your OpenGL application is memory intensive,
        // you should consider de-allocating objects that
        // consume significant memory here.
        mGLView.onPause();
    }
    @Override
    protected void onResume() {
        super.onResume();
        // The following call resumes a paused rendering thread.
        // If you de-allocated graphic objects for onPause()
        // this is a good place to re-allocate them.
        mGLView.onResume();
    }
}

군더더기 없는 그냥 액티비티다.위의 빨간색 코드를 보니 MyGLSurfaceView 인스턴스를 생성한다. 보통 Activity의 setContentView()에서 베이스 뷰를 셋팅하는데 마찬가지로 View를 상속 받은 MyGLSurfaceView를 넘겨주면 되겠다. MyGLSurfaceView의 소스코드를 보자.


2. MyGLSurfaceView

package com.example.android.opengl;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.view.MotionEvent;
/**
 * A view container where OpenGL ES graphics can be drawn on screen.
 * This view can also be used to capture touch events, such as a user
 * interacting with drawn objects.
 */
public class MyGLSurfaceView extends GLSurfaceView {
    private final MyGLRenderer mRenderer;
    public MyGLSurfaceView(Context context) {
        super(context);
        // Create an OpenGL ES 2.0 context.
        setEGLContextClientVersion(2);
        // Set the Renderer for drawing on the GLSurfaceView
        mRenderer = new MyGLRenderer();
        setRenderer(mRenderer);
        // Render the view only when there is a change in the drawing data
        setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    }
    private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
    private float mPreviousX;
    private float mPreviousY;
    @Override
    public boolean onTouchEvent(MotionEvent e) {
        // MotionEvent reports input details from the touch screen
        // and other input controls. In this case, you are only
        // interested in events where the touch position changed.
        float x = e.getX();
        float y = e.getY();
        switch (e.getAction()) {
            case MotionEvent.ACTION_MOVE:
                float dx = x - mPreviousX;
                float dy = y - mPreviousY;
                // reverse direction of rotation above the mid-line
                if (y > getHeight() / 2) {
                    dx = dx * -1 ;
                }
                // reverse direction of rotation to left of the mid-line
                if (x < getWidth() / 2) {
                    dy = dy * -1 ;
                }
                mRenderer.setAngle(
                        mRenderer.getAngle() +
                        ((dx + dy) * TOUCH_SCALE_FACTOR));  // = 180.0f / 320
                requestRender();
        }
        mPreviousX = x;
        mPreviousY = y;
        return true;
    }

우선 GLSurfaceView를 상속 받는다. GLSurfaceView는 SurfaceView를 상속받는데 이 서피스뷰는 UI스레드와 분리된 별도의 그리기 전용 스레드를 갖는다. GLSurfaceView는 OpenGL을 위한 전용 뷰다. 그리고 예제에서는 onTouchEvent() 메소드를 오버라이드 해서 유저의 터치 이벤트를 처리하고 있다.

MyGLSurfaceView의 생성자인 MyGLSurfaceView()를 보자.

// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);

사용할 오픈지엘의 버전을 파라미터로 넘기면서 초기화를 하고 있다.

// Set the Renderer for drawing on the GLSurfaceView
mRenderer = new MyGLRenderer();
setRenderer(mRenderer);

랜더러를 초기화하고 GLSurfaceView에 이 랜더러를 등록한다.

// Render the view only when there is a change in the drawing 
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

랜더모드를 설정한다. 예제에선 GLSurfaceView.RENDERMODE_WHEN_DIRTY를 설정했는데 오픈지엘은 2가지 랜더모드가 있다.
예제에서 사용한 모드는 처음 만들 때 한번, 그리고 requestRender() 함수를 호출 했을 때. 이렇게 수동으로 개발자가 랜더링 타이밍을 조절할 수 있는 옵션이다.

나머지 하나는 RENDERMODE_CONTINUOUSLY인데 이름에서도 알 수 있다시피 생성 시점 부터 계속 랜더링을 수행한다. RENDERMODE_CONTINUOUSLY가 기본 값이다.

그리고 다음 메소드인 onTouchEvent()는 유저의 터치이벤트를 처리하는 함수다. 안드로이드 프로그래밍을 해봤다면 위의 코드를 이해하는데 큰 어려움은 없을 것이다.

mRenderer.setAngle(
    mRenderer.getAngle() +
    ((dx + dy) * TOUCH_SCALE_FACTOR));  // = 180.0f / 320
requestRender();

랜더러의 setAngle()과 getAngle() 메소드를 호출하고 마지막에 requestRender()함수를 호출하고 있다. 예제에서 랜더모드를 RENDERMODE_WHEN_DIRTY로 셋팅 했기 때문에 화면을 다시 그리려면 GLSurfaceView에게 requestRender()로 요청해야한다.

setAngle() 함수는 GLSurfaceView.Renderer 인터페이스를 구현한 MyGLRenderer 클래스인 것이다. 즉,  개발자가 편의로 만든 메소드다. 유저로부터 터치 위치를 받아 현재각도에 값을 더해 다시 각도를 셋팅한다.

이 예제가 어떤건지 첫 부분에 설명했는데 터치 무빙하면 가운데 삼각형이 회전한다. 손가락을 왼쪽으로 이동할 때와 오른쪽으로 이동할 때 회전방향이 다르다.

코드에 TOUCH_SCALE_FACTOR 를 곱해주고있다. 180/320이라는데 그럼 0.5624다. 1보다 작으니 터치를 하면 0.5만큼 감쇄돼 이동될 것이다. 터치민감도를 제어하기 위한 용도로 이런 곱셈을 해줬다.

이제 MyGLRenderer로 넘어가보자.


3. MyGLRenderer

package com.example.android.opengl;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import android.util.Log;
public class MyGLRenderer implements GLSurfaceView.Renderer {
    private static final String TAG = "MyGLRenderer";
    private Triangle mTriangle;
    private Square   mSquare;
    // mMVPMatrix is an abbreviation for "Model View Projection Matrix"
    private final float[] mMVPMatrix = new float[16];
    private final float[] mProjectionMatrix = new float[16];
    private final float[] mViewMatrix = new float[16];
    private final float[] mRotationMatrix = new float[16];
    private float mAngle;
    @Override
    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        // Set the background frame color
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        mTriangle = new Triangle();
        mSquare   = new Square();
    }
    @Override
    public void onDrawFrame(GL10 unused) {
        float[] scratch = new float[16];
        // Draw background color
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
        // Set the camera position (View matrix)
        Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
        // Calculate the projection and view transformation
        Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
        // Draw square
        mSquare.draw(mMVPMatrix);
        // Create a rotation for the triangle
        // Use the following code to generate constant rotation.
        // Leave this code out when using TouchEvents.
        // long time = SystemClock.uptimeMillis() % 4000L;
        // float angle = 0.090f * ((int) time);
        Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, 1.0f);
        // Combine the rotation matrix with the projection and camera view
        // Note that the mMVPMatrix factor *must be first* in order
        // for the matrix multiplication product to be correct.
        Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
        // Draw triangle
        mTriangle.draw(scratch);
    }
    @Override
    public void onSurfaceChanged(GL10 unused, int width, int height) {
        // Adjust the viewport based on geometry changes,
        // such as screen rotation
        GLES20.glViewport(0, 0, width, height);
        float ratio = (float) width / height;
        // this projection matrix is applied to object coordinates
        // in the onDrawFrame() method
        Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
    }
    public static int loadShader(int type, String shaderCode){
        // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
        // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
        int shader = GLES20.glCreateShader(type);
        // add the source code to the shader and compile it
        GLES20.glShaderSource(shader, shaderCode);
        GLES20.glCompileShader(shader);
        return shader;
    }
    public static void checkGlError(String glOperation) {
        int error;
        while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
            Log.e(TAG, glOperation + ": glError " + error);
            throw new RuntimeException(glOperation + ": glError " + error);
        }
    }
    public float getAngle() {
        return mAngle;
    }
    public void setAngle(float angle) {
        mAngle = angle;
    }
}

우선 큰 구조를 보자

MyGLRenderer는 상속이 아니라 GLSurfaceView.Renderer 인터페이스를 구현했다.
3개의 메소드를 구현해야되는데,

onDrawFrame()
onSurfaceChanged()
onSurfaceCreated()

이렇게 3개다. 대충 이름을 보면 용도의 유추가 가능하다. onDrawFrame()은 그릴 때의 시점이고
onSurfaceChanged()는 스마트폰이 세로모드에서 가로모드로 변경될 때 등에 호출되겠고
onSurfaceCreated()는 처음 만들어질 때 호출되겠다.
그리고 멤버 변수 중 이런 배열이 아래처럼 4개가 있다.

private final float[] mMVPMatrix = new float[16];
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];
private final float[] mRotationMatrix = new float[16];


각 배열은 float형 16개의 값을 담을 수 있다. 왜 16개냐면 4*4의 행렬을 표현하고 있기 때문이다.

mMVPMatrix 는 주석에도 나와있지만 모델(Model), 뷰(View), 프로젝션(Projection) 메트릭스(행렬)의 줄임말이다. ‘모델’은 이 예제에서 삼각형을 의미하고 '뷰'는 우리의 시점을 의미한다. 예제에선 모델방향으로 정면을 바라보고 있다. 프로젝션은 3차원 투영을 의미한다. 아쉽게도 예제에선 3차원 느낌이 별로 없다.

그리고  mMVPMatrix 밑에 3개의 행렬이 더 있다. mProjectionMatrix는 바로 위에서 말한 3차원 투영값을 정의한 행렬이고 mMVPMatrix에 곱해질 것이다.
역시 mViewMatrix 행렬은 '어디를 바라봐라'라는 값이 저장된 행렬이고 mMVPMatrix에 곱함으로서 반영이 되겠다.
mRotationMatrix은 회전에 대한 값이 저장되겠고 역시 mMVPMatrix에 적용이 될것이다.
정리하자면 제일 위의 행렬인 mMVPMatrix는 변환대상이 되는 값이겠고 mProjectionMatrix, mViewMatrix, mRotationMatrix 들은 변환시킬 값들이 저장되겠다.
OpenGL함수를 이용해 이 변환값들을 mMVPMatrix에 적용하고 최종적으로 그릴 때는 mMVPMatrix을 사용한다. 밑에 함수들을 보시면 이해가 더 잘 될 것이다.

onSurfaceCreated()

GLSurfaceView가 생성될 때 한번 호출되는 메소드다. 이 때 값들을 초기화하면 되겠다.

GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

위 함수는 서비스뷰의 배경색을 설정한다. 각각의 인자는 무엇을 의미할까?
https://www.khronos.org/opengles/sdk/docs/man/
위 링크는 openGL ES 2.0의 레퍼런스 페이지이다. 문서를 보니 아래처럼 정의되어있다.

void glClearColor(GLclampf red,
  GLclampf green,
  GLclampf blue,
  GLclampf alpha);

파라미터 자료형은 GLclampf라는데 이건 잠시 뒤에 보고 파라미터 이름을 보면 red, green, blue, alpha 로 되어있다. 즉 RGBA형의 값을 넣어주면 되는 것이다. 예제에선 0,0,0,1을 넣었으니 검정색(0,0,0)이면서 투명도(1)가 아예없이 배경색이 칠해진다.

GLclampf는 어떤 자료형일까? 아래 위키문서를 보자.
https://www.opengl.org/wiki/OpenGL_Type
위 문서에서 찾아보니 32비트면서 0~1의 값을 갖는 값이란다.
보통 24비트 색상을 표현할 때  RGB채널 별로 8비트가 할당되고 채널당 256가지 색상을 표현할 수 있다. 이를 1로 정규화한 값을 넣어야한다. 즉 255값 중 100을 넣고 싶으면 100/256을 해주면 되겠다.

mTriangle = new Triangle();
mSquare   = new Square();

모델 클래스들을 초기화한다. 이 객체들은 openGL API는 아니고 예제 작성자가 편의상 만든 객체들이다. 위에서 설명한 대로 자기 자신에 대한 그리기를 담고있다. 일종의 캡슐화된 상태이다.

public void onDrawFrame(GL10 unused)

이름에서 알 수 있듯이 openGL이 그리는 타이밍에서 호촐하는 메소드다.

// Draw background color
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

* GL_COLOR_BUFFER_BIT 인자는 위에서 glClearColor로 설정한 색상으로 화면을 초기화하라는 것
* 뎁스버퍼(깊이 버퍼)도 초기화 하라는 것

// Set the camera position (View matrix)
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

시점의 값들을 초기화한다.

API문서는 다음과 같이 기록되어있다.
http://developer.android.com/reference/android/opengl/Matrix.html#setLookAtM(float[], int, float, float, float, float, float, float, float, float, float)

첫번째 인자는 결과값을 받을 메트릭스(배열)
두번째는 첫번째 인자로 들어온 배열 중 어디 값부터 결과가 시작되는지에 대한 offset값
다음 3개의 인자는 눈의 위치 (0,0,-3)
다음 3개의 인자는 눈의 방향 (0,0,0)
다음 3개의 인자는 눈의 윗방향(0,1,0). 이렇게 3개의 방향값을 전달한다.
이 3개의 좌표는 일종의 방향을 갖고있기 때문에 각각을 벡터라고도 할 수 있다. 즉 벡터 3개로 카메라(눈)의 위치와 방향을 지정한다.

// Calculate the projection and view transformation
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);

위에서 만든 시점 행렬(mViewMatrix)에 투영행렬(mProjectionMatrix)을 곱한 결과를 mMVPMatrix로 반환한다.
mProjectionMatrix 행렬은 투영행렬인데, 이 값은 onSurfaceChanged() 메소드에서 미리 환경에 맞게 셋팅되어있다. 컴퓨터 그래픽스에서 3D와 2D의 가장 큰 차이라면 이 투영이라 할 수 있다. 3차원 상(x,y,z)의 좌표들을 2D로 보여주는 방식을 결정하는 값들이라 생각하면 되겠다. multiplyMM 메소드는 OpenGL에서 범용이다. 배열내의 값들을 4x4 행렬로 인식해주고 이 두 행렬을 곱해준다.

// Draw square
mSquare.draw(mMVPMatrix);

현재 카메라 시점정보와 투영정보를 갖고 mSquare는 자신을 그려낸다. 이름에서 알 수 있듯이 사각형을 그린다. 이제 삼각형을 그려보자.

Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, 1.0f);

회전을 시킨다. mAngle은 회전각이고 이를 mRotationMatrix 배열의 적당한 위치에 기록한다.
두번째 인자는 해당 배열 중 어디부터 회전행렬인지를 알리는 offset값이다. 이거 다 0이 당연한거 아냐라고 생각할 수 있다. 지금은 각 행렬 별로 새로운 배열을 생성해서 그렇지만 배열 하나에 모든 행렬을 담는 경우 offset 값이 중요해진다.
세번째 mAngle은 회전할 각도 (radian이 아니라 도단위)
네번째부터 x, y, z값이며 이 세개의 좌표가 만드는 직선이 회전 축이 된다. 여기선
x=0, y=0, z=1 이 되겠다.

Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);

또 다시 Matrix.multiplyMM() 메소드가 등장했다.
mRotationMatrix과 mMVPMatrix을 곱해서 scratch 메트릭스에 기록한다. 주석을 보면, 이 순서를 바꾸지 말라고한다. 행렬은 교환법칙이 성립하지 않기 때문이다. 즉 일반적인 수들의 곱셈에서 2*3이나 3*2나 6이라는 결과가 변하지 않지만 행렬은 이 순서가 바뀌면 결과도 바뀐다.

mTriangle.draw(scratch);

주석을 보니 이제 삼각형을 그린다고 한다. 삼각형 객체에게 그리기를 위임한다. 즉 삼각형 객체가 현재 카메라 상태, 투영값 등 넘겨주는 환경정보를 바탕으로 알아서 그리라고 위임을 하는 것이다.

다시 한번 onDrawFrame() 메소드까지를 정리해보자.
최초에 OpenGLES20Activity로 시작을 했고 여기서 GLSurfaceView를 상속받은 MyGLSurfaceView를 생성했다.
MyGLSurfaceView에서 GLSurfaceView.Renderer를 구현(implements)한 MyGLRenderer를 생성해서 랜더러로 등록해줬다. 그리고 랜더링 타임을 setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
로 정했다. 즉 계속 랜더링하는게 아니라 데이터가 변경됐을 때만 랜더링하겠다는 방식이다. 그리고 여기서 public boolean onTouchEvent(MotionEvent e) 메소드를 오버라이드 했다.
유저가 터치를 통해 예제를 핸들링하고 싶어서다. onTouchEvent() 메소드의 구현내용 중 mRenderer.setAngle()를 통해 랜더러에게 현재 각도를 보낸다.
setAngle()메소드는 오픈지엘에서 제공하는게 아니라 우리가 원하는 값을 전달하고 싶어서 만든 메소드다. 그리고 requestRender() 메소드를 연이어 호출한다. requestRender()를 통해 다시 화면에 그려달라는 랜더링 요청을 openGL에 하는 것이다. 이 requestRender() 메소드가 호출되면 openGL은 GLSurfaceView.Renderer를 구현한 객체의 onDrawFrame() 메소드를 호출한다.

onDrawFrame() 메소드를 다시 정리해보자. 이 메소드가 호출되는 타이밍은 유저가 터치해서 움직일 때다. 이 때 계속 화면을 지우고 다시 그리고를 반복하게 된다. 스마트폰은 그림을 지웠다 다시 그리고 지웠다 다시 그리고 하지만 유저는 삼각형이 회전하는 것처럼 보이는것이다.

그리고 보는 시점(카메라)의 행렬을 만든다. setLookAtM 메소드로 쉽게 했다. 이 시점행렬과 투영행렬을 곱한 값을 사각형객체에 넘긴다. 그리고 이를 바로 삼각형에 넘기지는 않았고 뭔가 더 처리했다. setRotateM()인데 유저가 손가락을 움직 일 때 마다 사각형은 움직이지 않지만 삼각형은 회전을 한다. 이 회전행렬을 만들어 다시 ‘투영 X 시점’ 행렬에 곱해서 삼각형객체에 넘겼다.

onSurfaceChanged()
이제 onSurfaceChanged() 메소드의 구현부를 보자.

@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
    // Adjust the viewport based on geometry changes,
    // such as screen rotation
    GLES20.glViewport(0, 0, width, height);
    float ratio = (float) width / height;
    // this projection matrix is applied to object coordinates
    // in the onDrawFrame() method
    Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}

소스를 보니 Matrix.frustumM() 메소드로 최종 투영행렬 mProjectionMatrix를 만들어낸다.
그럼 이 frustumM 메소드를 보자. (구글 Android Dev Api)


frustumM (float[] m, int offset, float left, float right, float bottom, float top, float near, float far)

첫번째 인자는 결과값을 받을 행렬이고 역시 offset은 넘긴 배열의 어디 부터가 저장하라는 offset이다.
나머지 left, right, bottom, top, near, far가 있는데 기하학을 보면 절두체(frustum)라는게 있다. 이 절두체는 이렇게 생겼다.



http://en.wikipedia.org/wiki/Viewing_frustum#mediaviewer/File:ViewFrustum.svg

즉 이 절두체를 정의하는 메소드이다. 우리가 만든 3D객체들은 저 절두체안에 들어오는 것만 우리의 스마트폰 디스플레이에 출력된다. 중간에 걸치는건 몇가지 알고리즘에 의해서 오픈지엘이 알아서 짤라 준다.

절두체 사각형의 가운데 점을 중심으로 각각의 면들의 거리를 정해주는 것이다. 오픈지엘에서 좌표는 상대적인 것이라 0~1값들로 정의해도 되고 100000같은 큰 단위로 해줘도 상관없다. 하지만 보통 0~1 사이의 정규화된 값이 일반적이다. 계산할 때 소수점 이하만 신경쓰면 된다.

여기선
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
이렇게 값을 정해줬다.

left와 right가 -ratio,
bottom이 -1, top이 1,
near가 3, far가 7이다.

즉 아래가 -1, 위가 1 기준으로 앞(near)은 3, 뒤(far)가 7이다. 우리의 스마트폰의 가운데로 부터 왼쪽까지의 거리의 3배만큼이 앞이고 7배만큼이 뒤이다. 너무 가까워서 객체가 우리의 위치로부터 2에 있다면 이 절두체를 벗어나 우리가 볼 수 없겠고 마찬가지로 너무 뒤에 있어 7을 벗어난다면 역시 절두체로부터 짤려 우리가 볼 수 없게된다. 좌우위아래 역시 마찬가지다.
다른건 다 상수로 정해졌는데 left, right가 변수인 ratio다.

GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;

즉 이 값은 ‘가로 / 세로’ 화면 비율로 정해진다. 즉 우리의 스마트폰을 오픈지엘의 SurfaceView로 가득 채우기 위한 것이다. bottom, top을 1로 잡고 옆을 가변으로 잡은 것이다. 스마트폰이 가로모드에서 세로모드로 변경됐을 때도 이 값이 변경되겠다. 역시 화면을 꽉 채울것이다.

public static int loadShader(int type, String shaderCode)

이 메소드는 Square.java와 Triangle.java에서만 사용하니 해당 객체를 설명할 때 같이 보자.


4. Square.java

글의 서두에서도 썼듯이 이 예제에선 이 Square와 Triangle이 핵심인것 같다. 소스를 쭉 훑어보시면 아시겠지만 모습이 범상치 않다.

public class Square {
    private final String vertexShaderCode =
            // This matrix member variable provides a hook to manipulate
            // the coordinates of the objects that use this vertex shader
            "uniform mat4 uMVPMatrix;" +
            "attribute vec4 vPosition;" +
            "void main() {" +
            // The matrix must be included as a modifier of gl_Position.
            // Note that the uMVPMatrix factor *must be first* in order
            // for the matrix multiplication product to be correct.
            "  gl_Position = uMVPMatrix * vPosition;" +
            "}";
    private final String fragmentShaderCode =
            "precision mediump float;" +
            "uniform vec4 vColor;" +
            "void main() {" +
            "  gl_FragColor = vColor;" +
            "}";
    private final FloatBuffer vertexBuffer;
    private final ShortBuffer drawListBuffer;
    private final int mProgram;
    private int mPositionHandle;
    private int mColorHandle;
    private int mMVPMatrixHandle;
    // number of coordinates per vertex in this array
    static final int COORDS_PER_VERTEX = 3;
    static float squareCoords[] = {
            -0.5f,  0.5f, 0.0f,   // top left
            -0.5f, -0.5f, 0.0f,   // bottom left
             0.5f, -0.5f, 0.0f,   // bottom right
             0.5f,  0.5f, 0.0f }; // top right
    private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices
    private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
    float color[] = { 0.2f, 0.709803922f, 0.898039216f, 1.0f };
    /**
     * Sets up the drawing object data for use in an OpenGL ES context.
     */

    public Square() {
        // initialize vertex byte buffer for shape coordinates
        ByteBuffer bb = ByteBuffer.allocateDirect(
        // (# of coordinate values * 4 bytes per float)
                squareCoords.length * 4);
        bb.order(ByteOrder.nativeOrder());
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(squareCoords);
        vertexBuffer.position(0);
        // initialize byte buffer for the draw list
        ByteBuffer dlb = ByteBuffer.allocateDirect(
                // (# of coordinate values * 2 bytes per short)
                drawOrder.length * 2);
        dlb.order(ByteOrder.nativeOrder());
        drawListBuffer = dlb.asShortBuffer();
        drawListBuffer.put(drawOrder);
        drawListBuffer.position(0);
        // prepare shaders and OpenGL program
        int vertexShader = MyGLRenderer.loadShader(
                GLES20.GL_VERTEX_SHADER,
                vertexShaderCode);
        int fragmentShader = MyGLRenderer.loadShader(
                GLES20.GL_FRAGMENT_SHADER,
                fragmentShaderCode);
        mProgram = GLES20.glCreateProgram();             // create empty OpenGL Program
        GLES20.glAttachShader(mProgram, vertexShader);   // add the vertex shader to program
        GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
        GLES20.glLinkProgram(mProgram);                  // create OpenGL program executables
    }
    /**
     * Encapsulates the OpenGL ES instructions for drawing this shape.
     *
     * @param mvpMatrix - The Model View Project matrix in which to draw
     * this shape.
     */

    public void draw(float[] mvpMatrix) {
        // Add program to OpenGL environment
        GLES20.glUseProgram(mProgram);
        // get handle to vertex shader's vPosition member
        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
        // Enable a handle to the triangle vertices
        GLES20.glEnableVertexAttribArray(mPositionHandle);
        // Prepare the triangle coordinate data
        GLES20.glVertexAttribPointer(
                mPositionHandle, COORDS_PER_VERTEX,
                GLES20.GL_FLOAT, false,
                vertexStride, vertexBuffer);
        // get handle to fragment shader's vColor member
        mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
        // Set color for drawing the triangle
        GLES20.glUniform4fv(mColorHandle, 1, color, 0);
        // get handle to shape's transformation matrix
        mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
        MyGLRenderer.checkGlError("glGetUniformLocation");
        // Apply the projection and view transformation
        GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
        MyGLRenderer.checkGlError("glUniformMatrix4fv");
        // Draw the square
        GLES20.glDrawElements(
                GLES20.GL_TRIANGLES, drawOrder.length,
                GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
        // Disable vertex array
        GLES20.glDisableVertexAttribArray(mPositionHandle);
    }
}


아래 처음 코드 조각 부터 보자. 얘의 이름은 셰이더(Shader)다.
rivate final String vertexShaderCode =
            // This matrix member variable provides a hook to manipulate
            // the coordinates of the objects that use this vertex shader
            "uniform mat4 uMVPMatrix;" +
            "attribute vec4 vPosition;" +
            "void main() {" +
            // The matrix must be included as a modifier of gl_Position.
            // Note that the uMVPMatrix factor *must be first* in order
            // for the matrix multiplication product to be correct.
            "  gl_Position = uMVPMatrix * vPosition;" +
            "}";

일반적으로 3D 프로그래밍에서 셰이더는 크게 두가지다.
오픈지엘은 Vertex Shader, Fragment Shader라고 하는데
MS의 DirectX에선 Vertex Shader와 Pixcel Shader라고 하고
Apple의 Metal에선 Vertex Shader와 Graphics Shader라고한다. 다 용도가 비슷비슷하다.

우리의 코드를 보시면 역시 Vertext 셰이더와 Fragment 셰이더 2가지가 존재한다. 우선 버텍스 셰이더부터 보자. 위의 코드에서 주석을 제거해보자.

private final String vertexShaderCode =
            "uniform mat4 uMVPMatrix;" +
            "attribute vec4 vPosition;" +
            "void main() {" +
            "  gl_Position = uMVPMatrix * vPosition;" +
            "}"; 

셰이더란 gpu에서 고속 연산을 위해 작성하는 코드 조각(프로그램)이다. 이 코드 조각은 좌표별로 연산된다. 즉 사각형만 넘겼을 경우 4개의 좌표가 있을 것이고 그럼 위 코드조각은 4개가 동시에 실행된다. 보통 하드웨어 가속이란 말을 많이 쓰는데 이 말에 빠진 단어가 있다. 바로 'GPU를 이용한 하드웨어 가속’이다. 우리가 작성하는 코드들은 cpu를 위한 코드가 대부분이고, gpu를 위한 코드가 위의 코드다. gpu가 위의 문법을 바로 인식하지는 못하고, cpu 처럼 몇 가지 어셈블러 코드(Operation)로 이루어져야한다. 어셈블리어(Assembly language)란 010101의 기계어를 인간이 알기 쉽게 단순 영단어로 표현한 언어라는 것을 알것이다.

위에선 String객체로 만들었는데 게임이나 큰 프로그램에선 보통 외부 파일로 빼거나 서버로부터 받아 사용한다. 셰이더는 별도의 그래픽스 전문 프로그램 등으로 생성가능하고 테스트도 해야하고 쉽게 교체가 가능하기 때문이다. 안드로이드 앱은 이 셰이더를 단순히 gpu에 전달하는 역할만한다.

작은 프로그램이나 이 예제에선 그냥 String으로 만들지만 테스트를 위해서라도 현장에선 별도로 빼고 FileStream 등으로 읽어들이는게 좋다.
첫줄부터 보자.

uniform mat4 uMVPMatrix

uniform이란 일종의 상수다. mat4란 4차원의 Matrix라는 자료형이고 uMVPMatrix가 이 상수의 이름이다. 즉 4x4행렬 선언문인 것이다. 상수인이유는 값이 한번 정해지면 바뀔이유가 없다는 것인데 이름이 uMVPMatrix다. 즉 우리가 위의 onDrawFrame() 메소드에서 봤던 mMVPMatrix와 같은 값이겠다. 우리의 시점과 3D투영의 값들은 한번 정해주면 3D 연산시 바뀔 이유가 없기 때문에 상수로 정의한 것이다.

attribute vec4 vPosition

attribute란 변수고 vec4라는건 3차원 좌표를 의미한다. 4개의 원소를 갖는 벡터란 의미다. 원소가 4개인 이유는 4차원 행렬과 곱셈을 해야하기 때문이다. 왜 행렬이나 좌표들이 3차원이 아닌 4차원인지는 3D이론이기에 이 글에선 생략한다.

void main() {
 gl_Position = uMVPMatrix * vPosition;
};

위에 등장한 상수(uMVPMatrix)와 변수(vPosition)를 곱해서 gl_Position에 할당한다.
gl_Position은 우리가 선언하지 않았지만 openGL에서 미리 정의해둔 변수다. 이름에서 알 수 있듯이 좌표의 위치값을 넘겨 받는다. 우리는 우리가 만든 두개의 값을 곱해서 openGL에게 넘긴 것이다.

"이런 단순 연산은 JAVA에서 하면 되지 왜 셰이더에서 하나?" 라는 의문을 갖을 수 있다. 여기가 GPU의 ‘하드웨어 가속’의 의미가 빛을 발하는 순간이겠다. 우리 예제에서 사각형(Square)은 4개의 3차원 좌표로 이루어져있다. 이 4개의 좌표를 uMVPMatrix 와 곱해주려면 for문을 돌면서 cpu가 ‘순차적으로’ 곱해줘야한다.
하지만 gpu의 경우 동시에 4개의 좌표를 계산할 수 있다. 즉 동시에 출발해서 거의 동시에 끝난다는 얘기다. cpu에서 스레드를 4개 만들어도 코어가 4개인 cpu라면 동시 연산이 가능할것이다. 비슷할 수도 있으나 수천, 수만개라면 얘기가 달라지겠다. gpu의 코어가 300개라고 하면 100개 좌표의 연산은 순식간에 병렬처리되어 끝날 것이다. 하지만 cpu의 경우 코어가 아무리 많은 8개라도 코어당 처리해야할 좌표는 여전히 많다.

그리고 gpu는 3차원 좌표의 4칙연산에 최적화된 하드웨어 구조를 갖고있어서 단순연산 속도도 cpu 보다 훨씬 빠를것이다. 즉 그래픽스에 필요한 연산들을 미리 하드웨어로 구현해 놓은 것이다. 요즘은 General-purpose computing 이라고 해서 이런 gpu의 병렬처리의 장점을 그래픽스 연산뿐만 아니라 다른 영역에서도 활용할 수 있게 구조를 제공하는게 추세이다. 물리, 과학데이터 분석이나 통계적 빅데이터같은 대량 연산에서도 사용할 수 있는 것이다. nvidia의 CUDA나 openCL 등이 그런 용도로 만들어졌다. 어쨌든 이런 병렬처리의 이점을 많이 얻으려면 외부에서 잘 쪼개고 인풋과 아웃풋이 확실하고 단순한 조각으로 분해하는게 중요하다. 역시 아직도 간단치 않고 개발자의 역량이 중요하다 하겠다.

다음 코드는 프래그먼트 셰이더다.
private final String fragmentShaderCode =
            "precision mediump float;" +
            "uniform vec4 vColor;" +
            "void main() {" +
            "  gl_FragColor = vColor;" +
            "}";


군더더기를 제거하면 아래와 같다.

precision mediump float;
uniform vec4 vColor
void main() {
   gl_FragColor = vColor
}

첫번 째 줄에서 precision이란 이 프래그먼트 셰이더에서 부동소수점의 정밀도를 mediump로 설정하고 있다. 옵션은 lowp, mediump, highp 가 있으며 여기서는 중간으로 설정되어있다. 버텍스 셰이더는 프래그먼트 셰이더보다 저 정확해야 되기 때문에 highp 이 기본값으로 설정되어있으나 정교할 수록 성능은 떨어진다. 보통 프래그먼트 셰이더에선 정교함을 손해보는 대신 성능의 이점이 있는 mediump를 쓴다.

다음 줄을 보자.

uniform vec4 vColor

vColor에서 알 수 있듯이 4개의 원소를 갖는 색상값을 선언한다. 4개의 원소는 빨강, 녹색, 파랑 그리고 투명값으로 이루어져 있다. attribute이 매 정점마다 값이 달랐다면 이 값은 모든 정점의 색이 같다.
다음 줄에선 gl_FragColor에 색상 값을 전달한다. 오픈지엘은 이 gl_FragColor 값을 최종적인 칼라값을 받는 채널로 사용하고 있다.

지금까지 본 셰이더의 역할을 정리해보자면, 버텍스 셰이더에선 좌표별로 투영행렬을 곱했고 프래그먼트 셰이더에선 모든 좌표에 같은 색상값을 할당했다. 사각형의 경우 4개의 점이며 이 세 점 모두 같은 색이고 이 점이 이루는 면도 같은 색으로 칠해진다.

그 다음줄들에 나오는 멤버변수들은 일단 건너 띄고

    static float squareCoords[] = {
            -0.5f,  0.5f, 0.0f,   // top left
            -0.5f, -0.5f, 0.0f,   // bottom left
             0.5f, -0.5f, 0.0f,   // bottom right
             0.5f,  0.5f, 0.0f }; // top right

위 배열에서 3개의 원소마다 하나의 정점을 이룬다. GPU 프로그래밍에선 1차원 배열과 첨자를 활용하는 방식이 일반적이다. 배열의 크기는 총 3 X 4의 12라 4개의 점을 갖는다. 사각형의 좌상단의 좌표는 -0.5f, 0.5f, 0f인데 x좌표값이 -0.5, y가 0.5, z가 0이라는 의미이다. 4개의 점의 z값이 모두 0이다. 입체감 없는 평평한 2차원의 사각형이란걸 알 수 있다. 좌표가 0.5라고 해서 0.5픽셀이 아니고 상대적인 비율로 간주하시는게 이해하기 쉽다. 실제 크기는 카메라의 상태값 + 투영값 등에 의해 우리 눈에 보이는 사각형의 크기와 모양은 매번 다르다.

private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices

버텍스들의 그리는 순서다. 여기서 잠깐 다시 3D 이론을 살펴보자. 컴퓨터 그래픽스의 3D 면들은 모두 삼각형으로 이루어져있다. 삼각형이 최소한의 점들로 평면을 만들 수 있기 때문이다. 유려한 곡선처럼 보이는것도 실은 아주 작은 삼각형들이 이루고 있는 연속된 직선들이다. 위의 drawOrder[] 배열은 그 삼각형을 그리는 순서다. 더 정확히 말하자면 삼각형 2개가 붙어 있는 사각형에서의 그리는 순서이다.

private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
float color[] = { 0.2f, 0.709803922f, 0.898039216f, 1.0f };


그림은 위 좌표 배열 squareCoords[]를 기반으로 그린 사각형이다. drawOrder[] 배열에서 처음 3개의 요소의 값은 0, 1, 2 이다. 위의 그림에서 삼각형을 그리는 순서라고 생각하면 된다. 다음 0, 2, 3 값이 오는데 오른쪽의 삼각형을 그리는 순서가 되겠다. 보면 두 삼각형 모두 시계 반대 방향으로 진행했다. 오픈지엘은 삼각형의 앞면을 표시할 때 이렇게 그려주면 된다. 뒷면은 0, 2, 1 순으로 그리면 되겠지만 보통 앞면을 그리고 뒤집는 방식을 취한다.

private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
float color[] = { 0.2f, 0.709803922f, 0.898039216f, 1.0f };


public Square()
Square클래스의 생성자다. 생성자에선 주로 위에서 봤던 좌표와 그리기 순서값을 버퍼에 담아 GPU에 넘겨주고 역시 위에서 만든 셰이더 코드를 GPU에 탑재한다.

// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);

ByteBuffer.allocateDirect() 메소드로 특정 크기의 바이터 버퍼를 생성한다. 크기는 사각형을 이루는 좌표 12개 * 4를 해줬다. 즉 한 정점당 4바이트의 크기를 갖게 되는 것이다. 자바에서 하나의 float 값은 4byte이다.

vertexBuffer = bb.asFloatBuffer();

Big-endian인지 Little-endian인지 바이트의 오더값을 정해준다. 바이트의 순서는 GPU마다 다를 수 있다. 우리는 우리가 그리고 싶어하는 정보들을 GPU에게 넘겨야하는데 그 GPU의 입맛에 맞게 맞춰주는 것이다.

vertexBuffer = bb.asFloatBuffer();

ByteBuffer를 FloatBuffer 형식으로 맞춰준다.

vertexBuffer.put(squareCoords);
vertexBuffer.position(0);

사각형 좌표들을 넘기고 위치를 0으로 맞춰줬다.


 // initialize byte buffer for the draw list
        ByteBuffer dlb = ByteBuffer.allocateDirect(
                // (# of coordinate values * 2 bytes per short)
                drawOrder.length * 2);
        dlb.order(ByteOrder.nativeOrder());
        drawListBuffer = dlb.asShortBuffer();
        drawListBuffer.put(drawOrder);
        drawListBuffer.position(0);

마찬가지로 drawOrder[] 배열값을 GPU에 넘기기 위해 바이트버퍼를 만든다. 이전과는 다르게 4바이트 기준에서 2바이트로 바꼈는데 정점과는 달리 그리기 순서는 값이 0,1, 2 등으로 값이 매우 작기 때문이다. 메모리 낭비를 줄일 수 있겠다.


// prepare shaders and OpenGL program
        int vertexShader = MyGLRenderer.loadShader(
                GLES20.GL_VERTEX_SHADER,
                vertexShaderCode);
        int fragmentShader = MyGLRenderer.loadShader(
                GLES20.GL_FRAGMENT_SHADER,
                fragmentShaderCode);
        mProgram = GLES20.glCreateProgram();             // create empty OpenGL Program
        GLES20.glAttachShader(mProgram, vertexShader);   // add the vertex shader to program
        GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
        GLES20.glLinkProgram(mProgram);                  // create OpenGL program executables

각각의 셰이더를 GPU에 탑재하고 연결하는 코드다.
이 연결을 바탕으로 아래 draw() 메소드에서 그리기를 수행한다.

public void draw(float[] mvpMatrix)

이 메소드는 랜더링 할 때 마다 호출되며 유저가 터치할 때 마다 호출될 것이다. 주로 위에서 링크한 셰이더의 변수와 상수들에 값을 넣어주어 셰이더가 연산을 수행하게 된다. 자바에서 GPU에 있는 셰이더의 변수와 상수에 접근하려면 Handler를 구해와야한다. 그 핸들러를 통해 값을 넣어주게 된다. 셰이더 코드를 보면 버텍스 셰이더에서 2개(uMVPMatrix, vPosition), 프래그먼트 셰이더에서 1개(vColor)이다.

// Add program to OpenGL environment
GLES20.glUseProgram(mProgram);

우리가 위에서 만든 셰이더의 핸들러인 mProgram을 사용하겠다고 오픈지엘에 알린다.

// get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, “vPosition");

언급한 구해야할 핸들러 3개중 첫번째다. 버텍스 셰이더인 vertexShaderCode에서 정점을 의미하는 변수인 vPosition의 핸들러를 구한다. GPU에 탑재된 셰이더에서 해당 변수 값의 핸들러를 구해오는 것이다.

// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(mPositionHandle);

openGL 에게 vPosition이 정점좌표의 변수라는 것을 알린다.

// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(
        mPositionHandle, COORDS_PER_VERTEX,
        GLES20.GL_FLOAT, false,
        vertexStride, vertexBuffer);

셰이더의 ‘vPosition’ 변수에 정의한 버텍스(좌표)들을 전달한다. glVertexAttribPointer() 함수에 대한 상세 스펙은 다음의 문서에서 참고한다. https://www.khronos.org/opengles/sdk/docs/man/xhtml/glVertexAttribPointer.xml

// get handle to fragment shader's vColor member
mColorHandle = GLES20.glGetUniformLocation(mProgram, “vColor");

이제는 프래그먼트 셰이더차례다. 마찬가지로 프래그먼트 셰이더의 vColor의 핸들러를 구한다. 위에서 vColor는 상수였음을 설명했다.

// Set color for drawing the triangle
GLES20.glUniform4fv(mColorHandle, 1, color, 0);

구한 핸들러를 통해 vColor 상수에 color값을 대입한다.

// get handle to shape's transformation matrix
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
MyGLRenderer.checkGlError(“glGetUniformLocation");

이제 사각형에 투영값을 연산해야한다. 그럴려면 셰이더에서 Matrix 변수의 핸들값을 구해야하는데 위 코드에서 셰이더의 uMVPMatrix 변수의 핸들을 구했다.
MyGLRenderer.checkGlError() 메소드는 방금 수행했던 glGetUniformLocation() 함수에서 에러는 없었는지 확인한다.

// Apply the projection and view transformation
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
MyGLRenderer.checkGlError(“glUniformMatrix4fv");

Matrix핸들러를 통해 투상과 관련된 Matrix를 전달하고 에러를 확인한다.

// Draw the square
GLES20.glDrawElements(
        GLES20.GL_TRIANGLES, drawOrder.length,
        GLES20.GL_UNSIGNED_SHORT, drawListBuffer);

이제 그릴 준비가 끝났다. glDrawElements() 함수에 몇가지 옵션을 적용해 삼각메쉬 그리기를 수행한다. 그리기 명령은 glDrawElements 외에도 glDrawArray가 있으나 보통 성능 이슈도 있어서 glDrawElements()가 일반적이다.

// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);

draw()함수 초반에 등장했던 glEnableVertexAttribArray() 함수와 반대되는 기능을 갖는다.
위의 코드는 mPositionHandle 값을 다시 무력화 시킨다. 이 예제에선 없어도 되는 코드지만 추가 하위 로직을 구현하면서 mPositionHandle에 다른 값을 넣어야할 때 필요하겠다.

Square객체를 정리해보자.
멤버 변수에선 미리 정의되어야할 것들이 있었다. 2개의 프래그먼트와 그리고자하는 사각형의 정점, 그리는 순서에 대한 정보, 마지막으로 색상.
생성자인 Square()에선 먼저 사각형을 의미하는 정점과 그리는 순서를 버퍼에 담았고 2개의 셰이더 코드를 GPU로 전달했다. 내부적으론 받은 셰이더 코드를 컴파일했을 것이다.
그리고 draw()함수에선 셰이더의 각종 변수와 상수들을 핸들러로 구했고 이 핸들러와 openGL함수를 통해 준비된 정점과 컬러, 투영행렬(Matrix) 데이터를 셰이더 프로그램에 전달했다. 최종 glDrawElements함수를 통해 gpu는 셰이더 연산을 수행했을 것이다.

5. Triangle

이제 마지막으로 삼각형 객체인 Triangle을 보자. 거의 똑같고 삼각형이라 몇가지 차이점만 존재한다. 이 차이점들만 보고 마무리한다.

그리기 순서가 존재하지 않는다. 단순한 삼각형이라 배열에 담긴 좌표 순서대로 그리면 된다.
Square에선 draw()함수에서 glDrawElements() 함수를 통해 그렸지만 Triangle은 glDrawArrays()를 이용해서 그렸다. glDrawElements()는 어떤 순서대로 사각형을 그릴지를 인자로 넘겼지만 glDrawArrays()는 그냥 순서대로 그리라는 명령이다. 이 코드상으로라면 그냥 그리면 삼각형 하나가 덜렁 그려진다.

1267 view

4.0 stars