도형 그리기

OpenGL로 그릴 도형을 정의한 후 도형을 그릴 가능성이 높습니다. OpenGL ES 2.0으로 도형을 그리려면 생각하는 것보다 조금 더 많은 코드가 필요합니다. 왜냐하면 API에서 그래픽 렌더링 파이프라인에 대한 세심한 제어를 요구하기 때문입니다.

이 교육 과정에서는 OpenGL ES 2.0 API를 사용하여 이전 과정에서 정의한 도형 그리기 방법을 설명합니다.

도형 초기화

그리기 전에 그릴 도형을 초기화하고 로드해야 합니다. 프로그램에서 사용하는 도형 구조(원래 좌표)가 실행 과정에서 변경되지 않으면 메모리와 처리 효율성을 위해 렌더기의 onSurfaceCreated() 메서드에서 초기화해야 합니다.

Kotlin

    class MyGLRenderer : GLSurfaceView.Renderer {
        ...
        private lateinit var mTriangle: Triangle
        private lateinit var mSquare: Square

        override fun onSurfaceCreated(unused: GL10, config: EGLConfig) {
            ...
            // initialize a triangle
            mTriangle = Triangle()
            // initialize a square
            mSquare = Square()
        }
        ...
    }
    

자바

    public class MyGLRenderer implements GLSurfaceView.Renderer {

        ...
        private Triangle mTriangle;
        private Square   mSquare;

        public void onSurfaceCreated(GL10 unused, EGLConfig config) {
            ...
            // initialize a triangle
            mTriangle = new Triangle();
            // initialize a square
            mSquare = new Square();
        }
        ...
    }
    

도형 그리기

OpenGL ES 2.0을 사용하여 정의된 도형을 그리려면 상당한 양의 코드가 필요합니다. 그래픽 렌더링 파이프라인에 많은 양의 세부정보를 제공해야 하기 때문입니다. 특히 다음을 정의해야 합니다.

  • 꼭짓점 셰이더 - 도형의 꼭짓점을 렌더링하는 OpenGL ES 그래픽 코드입니다.
  • 조각 셰이더 - 색상 또는 질감으로 도형의 면을 렌더링하는 OpenGL ES 코드입니다.
  • 프로그램 - 하나 이상의 도형을 그리는 데 사용할 셰이더가 포함된 OpenGL ES 객체입니다.

도형을 그리려면 하나 이상의 꼭짓점 셰이더가 있어야 하고 도형의 색상을 지정하려면 하나의 조각 셰이더가 있어야 합니다. 이 셰이더를 컴파일한 다음 OpenGL ES 프로그램에 추가해야, 모양을 그리는 데 이 프로그램을 사용할 수 있습니다. 다음은 Triangle 클래스에서 도형을 그리는 데 사용할 수 있는 기본 셰이더의 정의 방법을 보여주는 예입니다.

Kotlin

    class Triangle {

        private val vertexShaderCode =
                "attribute vec4 vPosition;" +
                "void main() {" +
                "  gl_Position = vPosition;" +
                "}"

        private val fragmentShaderCode =
                "precision mediump float;" +
                "uniform vec4 vColor;" +
                "void main() {" +
                "  gl_FragColor = vColor;" +
                "}"

        ...
    }
    

자바

    public class Triangle {

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

        private final String fragmentShaderCode =
            "precision mediump float;" +
            "uniform vec4 vColor;" +
            "void main() {" +
            "  gl_FragColor = vColor;" +
            "}";

        ...
    }
    

셰이더에는 OpenGL ES 환경에서 사용하기 전에 컴파일해야 하는 GLSL(OpenGL Shading Language) 코드가 포함되어 있습니다. 이 코드를 컴파일하려면 다음과 같이 렌더기 클래스에 유틸리티 메소드를 만들어야 합니다.

Kotlin

    fun loadShader(type: Int, shaderCode: String): Int {

        // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
        // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
        return GLES20.glCreateShader(type).also { shader ->

            // add the source code to the shader and compile it
            GLES20.glShaderSource(shader, shaderCode)
            GLES20.glCompileShader(shader)
        }
    }
    

자바

    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;
    }
    

도형을 그리려면 셰이더 코드를 컴파일하고 OpenGL ES 프로그램 객체에 추가한 다음 프로그램을 링크해야 합니다. 그린 객체의 생성자에서 이 작업을 수행하므로 한 번만 수행됩니다.

메모: OpenGL ES 셰이더를 컴파일하고 프로그램을 링크하려면 CPU 주기 및 처리 시간이 많이 소요되므로 한 번에 완료하는 것이 좋습니다. 런타임 시 셰이더의 내용을 모르면 한 번만 만든 다음 나중에 사용하기 위해 캐시하도록 코드를 빌드해야 합니다.

Kotlin

    class Triangle {
        ...

        private var mProgram: Int

        init {
            ...

            val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
            val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)

            // create empty OpenGL ES Program
            mProgram = GLES20.glCreateProgram().also {

                // add the vertex shader to program
                GLES20.glAttachShader(it, vertexShader)

                // add the fragment shader to program
                GLES20.glAttachShader(it, fragmentShader)

                // creates OpenGL ES program executables
                GLES20.glLinkProgram(it)
            }
        }
    }
    

자바

    public class Triangle() {
        ...

        private final int mProgram;

        public Triangle() {
            ...

            int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
                                            vertexShaderCode);
            int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
                                            fragmentShaderCode);

            // create empty OpenGL ES Program
            mProgram = GLES20.glCreateProgram();

            // add the vertex shader to program
            GLES20.glAttachShader(mProgram, vertexShader);

            // add the fragment shader to program
            GLES20.glAttachShader(mProgram, fragmentShader);

            // creates OpenGL ES program executables
            GLES20.glLinkProgram(mProgram);
        }
    }
    

이제 도형을 그리는 실제 호출을 추가할 준비가 되었습니다. OpenGL ES로 도형을 그리려면 렌더링 파이프라인에 그릴 내용과 그리는 방법을 지시할 여러 매개변수를 지정해야 합니다. 그리기 옵션은 도형별로 다양하므로 도형 클래스에 고유한 그리기 로직을 포함시키는 것이 좋습니다.

도형을 그리는 draw() 메서드를 작성합니다. 이 코드에서는 도형의 꼭짓점 셰이더와 조각 셰이더에 위치와 색상 값을 설정한 다음 그리기 함수를 실행합니다.

Kotlin

    private var positionHandle: Int = 0
    private var mColorHandle: Int = 0

    private val vertexCount: Int = triangleCoords.size / COORDS_PER_VERTEX
    private val vertexStride: Int = COORDS_PER_VERTEX * 4 // 4 bytes per vertex

    fun draw() {
        // Add program to OpenGL ES environment
        GLES20.glUseProgram(mProgram)

        // get handle to vertex shader's vPosition member
        positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition").also {

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

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

            // get handle to fragment shader's vColor member
            mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor").also { colorHandle ->

                // Set color for drawing the triangle
                GLES20.glUniform4fv(colorHandle, 1, color, 0)
            }

            // Draw the triangle
            GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount)

            // Disable vertex array
            GLES20.glDisableVertexAttribArray(it)
        }
    }
    

자바

    private int positionHandle;
    private int colorHandle;

    private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
    private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex

    public void draw() {
        // Add program to OpenGL ES environment
        GLES20.glUseProgram(mProgram);

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

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

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

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

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

        // Draw the triangle
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);

        // Disable vertex array
        GLES20.glDisableVertexAttribArray(positionHandle);
    }
    

이 코드를 모두 작성한 후에는 렌더기의 onDrawFrame() 메서드에서 draw() 메서드를 호출하기만 하면 이 객체를 그릴 수 있습니다.

Kotlin

    override fun onDrawFrame(unused: GL10) {
        ...

        triangle.draw()
    }
    

자바

    public void onDrawFrame(GL10 unused) {
        ...

        triangle.draw();
    }
    

애플리케이션을 실행하면 다음과 같이 표시되어야 합니다.

그림 1. 투영 또는 카메라 보기 없이 그려진 삼각형.

이 코드 예에는 몇 가지 문제점이 있습니다. 먼저 그다지 인상적이거나 눈에 띄는 도형이 아닙니다. 또한 삼각형이 약간 찌그러져 있으며 기기의 화면 방향을 변경하면 도형이 변경됩니다. 도형이 비뚤어지는 이유는 객체의 꼭짓점이 GLSurfaceView가 표시되는 화면 영역의 비율에 맞게 수정되지 않았기 때문입니다. 다음 학습 과정에서 투영과 카메라 보기를 사용하여 이 문제점을 수정할 수 있습니다.

마지막으로 삼각형은 정지되어 있으므로 약간 지루한 느낌을 줍니다. 모션 추가 학습 과정에서 이 도형을 회전하게 만들고 OpenGL ES 그래픽 파이프라인을 더 흥미롭게 사용해 보겠습니다.