[Android] 3D API, OpenGL ES – 2 : 폴리곤 렌더링

이전 글을 통해 안드로이드에서 OpenGL ES에 대한 초기화에 대해 살펴보았습니다. 이 글은 간단한 폴리곤을 화면상에 렌더링해 보는 API에 대해 살펴보겠습니다. 간단한 폴리곤에 대한 렌더링을 위해 먼저 폴리곤을 구성하는 데이터에 대한 개념을 살펴보도록 하겠습니다. (OpenGL ES를 보시기 전에 먼저 OpenGL을 선행 학습하시면 훨씬 쉽게 이글을 이해할 수 있습니다.)

정점(Vertex) : 정점은 3D 모델을 구성하는 최소 단위입니다. 정점은 2개 이상의 모서리(Edge)가 만나는 점입니다. 3D 모델에서 정점은 모든 연결된 모서리나 면(Face) 그리고 폴리곤 사이에 공유됩니다. 또한 정점은 카메라나 광원의 위치를 나타내는데도 사용됩니다.

모서리(Edge) : 모서리는 두개의 정점을 잇는 선분입니다. 모서리는 면이나 폴리곤의 외곽선입니다. 3D 모델에서 모서리는 2개의 인접한 면이나 폴리곤 사이에 공유됩니다. OpenGL ES에서는 선분을 정의한다라는 개념보다는 정점을 이용해 면을 정의한다고 합니다. 면은 최소 3개의 모서리로 구성됩니다.

면(Face) : 면은 삼각형입니다. 면은 3개의 정점으로 구성되며 3개의 모서리로 둘러 싸여졌습니다. 면의 구성을 변환하면 모든 연결된 정점과 모서리 그리고 폴리곤에 영향을 받습니다. 면을 구성하는 정점의 지정 순서에 따라 면의 앞면과 뒷면에 대한 정의가 달라집니다. 앞면과 뒷면이 중요한 이유는 퍼포먼스에 있습니다. 뒷면은 눈에 들어나지 않으므로 렌더링에서 제외될 수 있기 때문입니다. 정점의 지정 순서에 대해 앞면이냐 뒷면이냐를 정의할 수 있는데 glFrontFace 매서드를 통해 가능합니다. 정점의 지정순서는 삼각형에 대해 반시계 방향 순서(CCW)냐 시계 방향 순서(CW)냐입니다. OpenGL ES는 기본적으로 반시계 방향이 앞면을 의미합니다.

폴리곤(Polygon) : 폴리곤은 하나의 3D 모델이라고 생각하면 됩니다. 폴리곤을 구성하기 위해서는 정점들의 데이터와 이 정점들 중 3개를 이용해 면을 구성하기 위한 정점 인덱스 정보를 통해 이뤄집니다. 폴리곤을 화면에 렌더링하는 매서드는 glDrawArrays와 glDrawElements입니다. 이 두 함수의 첫번째 인자는 정점과 인덱스를 통해 구성할 모델의 형태입니다. 다음과 같이 총 7가지 모드가 존재합니다.

1. GL_POINTS : 정점을 개별적인 포인트로 렌더링한다.

사용자 삽입 이미지
2. GL_LINE_STRIP : 정점을 지정된 순서대로 이어 라인을 구성해 렌더링한다.사용자 삽입 이미지
3. GL_LINE_LOOP : GL_LINE_STRIP와 동일하고 첫번째와 마지막 정점을 잇는다.사용자 삽입 이미지
4. GL_LINES : 정점 2개씩 연속적으로 묶어 개별적인 선분 하나씩 구성해 렌더링한다.사용자 삽입 이미지
5. GL_TRIANGLES : 정점 3개씩 연속적으로 묶어 개별적인 삼각형을 구성해 렌더링한다.사용자 삽입 이미지
6. GL_TRIANGLE_STRIP : 처음은 제공되는 3개의 정점으로 삼각형을 구성하고 다음 정점 하나를 통해 또 다른 삼각형을 구성한다. 구성되는 삼각형들은 모두 동일한 면 방향을 갖도록 구성하게 된다.사용자 삽입 이미지
7. GL_TRIANGLE_FAN : GL_TRIANGLES_STRIP와 비슷하지만 처음 제공되는 정점을 중심으로 팬(Fan) 형태로 삼각형을 구성해 렌더링한다.사용자 삽입 이미지
이제 이러한 기본 지식을 토대로 OpenGL ES를 통해 안드로이드에서 간단한 사각형 폴리곤 모델을 렌더링 해보는 코드를 작성해 보겠습니다. 기반이 되는 코드는 이전 글의 초기화에서 작성했던 프로젝트를 기반으로 합니다. 변경될 부분은 MyRenderer 클래스와 Square라는 새로운 클래스 추가입니다. 먼저 Square이라는 새로운 클래스를 추가합니다.

public class Square { }

이 클래스는 앞에서 언급했던 사각형 폴리곤 모델에 대한 정점과 정점 인덱스 정보를 담고 있으며 렌더링하는 코드가 존재합니다. 구성할 사각형 폴리곤의 정점 좌표와 인덱스 정보는 다음과 같습니다.

사용자 삽입 이미지
즉.. 총 4개의 정점으로 존재하며 총 2개의 삼각형 면으로 구성되어 있습니다. 2개의 삼각형면을 구성하기 위해 사용할 정점에 대한 인덱스가 필요할텐데.. 이에 대한 정보를 클래스에 코드로 추가합니다.

public class Square {
    private float vertices[] = {
        -1.0f,  1.0f, 0.0f,
        -1.0f, -1.0f, 0.0f,
        1.0f, -1.0f, 0.0f,
        1.0f,  1.0f, 0.0f,
    };
 
    private short[] indices = { 0, 1, 2, 0, 2, 3 };
}

vertices가 정점에 대한 정보를 담고 있으며 indices가 삼각형 면을 구성할 정점에 대한 인덱스 정보를 담고 있습니다. 이제 Square의 생성자에서 이 정보를 OpenGL ES에서 사용할 수 있도록 Buffer로 저장하는 코드를 작성합니다.

 private FloatBuffer vertexBuffer;
 private ShortBuffer indexBuffer;

public Square() {
    ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
    vbb.order(ByteOrder.nativeOrder());
    vertexBuffer = vbb.asFloatBuffer();
    vertexBuffer.put(vertices);
    vertexBuffer.position(0);

    ByteBuffer ibb = ByteBuffer.allocateDirect(indices.length * 2);
    ibb.order(ByteOrder.nativeOrder());
    indexBuffer = ibb.asShortBuffer();
    indexBuffer.put(indices);
    indexBuffer.position(0);
}

자바의 NIO를 이용해 배열을 ByteBuffer에 담아 정점에 대한 버퍼는 FloatBuffer 타입의 vertexBuffer로 정점 인덱스에 대한 버퍼는 ShortBuffer 타입의 indexBuffer에 담아 놓습니다. 이제 3D 모델에 대한 정의는 끝났고.. 이 모델을 렌더링해주는 코드를 Square에 추가해 보면 다음과 같습니다.

public void draw(GL10 gl) {
    gl.glFrontFace(GL10.GL_CCW);
    gl.glEnable(GL10.GL_CULL_FACE);
    gl.glCullFace(GL10.GL_BACK);
  
    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
    gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
    gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, 
        GL10.GL_UNSIGNED_SHORT, indexBuffer);
    gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
    gl.glDisable(GL10.GL_CULL_FACE);
 }

먼저 2번 코드는 앞면은 정점 지정 순서가 반시계 방향으로 지정한 것이고 3번 코드와  4번 코드를 통해 뒷면은 렌더링되지 않도록 합니다. 6번 코드는 정점 배열을 통해 모델을 렌더링하겠다고 지정한 코드이며 7번 코드는 모델의 정점 배열을 지정해 주는 코드입니다. 8번은 실제로 렌더링 시키라는 코드로 이 함수에 정점 인덱스 배열이 인자로 들어갑니다. 9번 코드는 정점 배열과 정점 인덱스 배열의 지정이 끝났음을 알립니다. 이제 이렇게 만들어진 Square 클래스를 사용하는 일이 남았습니다.

MyRenderer 클래스에 Square 클래스에 대한 필드 변수를 추가하고 onSurfaceCreated 매서드에서 생성합니다.

public class MyRenderer implements Renderer {
    private Square square;

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        square = new Square();

        gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        gl.glShadeModel(GL10.GL_SMOOTH);
        gl.glClearDepthf(1.0f);
        gl.glEnable(GL10.GL_DEPTH_TEST);
        gl.glDepthFunc(GL10.GL_LEQUAL);
        gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
    }    
}

onSurfaceCreated 매서드를 보면 Square 변수를 생성하는 코드 이외에도 OpenGL ES의 다양한 값들을 지정해 주는 코드가 존재합니다. 다음으로 MyRenderer 클래스의 onDrawFrame 매서드에서 square를 그리는 코드를 작성합니다.

@Override
public void onDrawFrame(GL10 gl) {
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

    gl.glLoadIdentity();
    gl.glTranslatef(0, 0, -10);
    square.draw(gl);
}

화면상에 사각형 모델이 보이도록 5번 코드에서 Z축으로 -10만큼 이동하였습니다. 이에 앞서 투영 행렬과 모델뷰 행렬을 지정해 줘야 하는데.. 이 코드는 MyRenderer의 onSurfaceChanged 매서드에서 실행됩니다.

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    gl.glViewport(0, 0, width, height);
  
    gl.glMatrixMode(GL10.GL_PROJECTION);
    gl.glLoadIdentity();
  
    GLU.gluPerspective(gl, 45.0f, (float)width/(float)height, 0.1f, 100.0f);
    gl.glMatrixMode(GL10.GL_MODELVIEW);
}

이제 실행해 보면 다음과 같은 결과를 얻을 수 있습니다.

사용자 삽입 이미지

“[Android] 3D API, OpenGL ES – 2 : 폴리곤 렌더링”에 대한 7개의 댓글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다