삼각형 만들기

지난 1. Android OpenGL ES의 개요 포스팅에서 간단히 삼각형을 만들었고, 삼각형을 그리기위해서 Activity, GLSurfaceView, Renderer가 필요했었습니다.

이번 시간에는 삼각형을 그리는 프로세스에 대해서 알아보도록 하겠습니다.

안드로이드 프로젝트 설정

디바이스가 OpenGL ES 2.0을 지원해야하기에 다음과 같이 메니페스트에 선언해줍니다.

<manifest>
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
...
</manifest>

MainActivity.kt

OpenGL ES 2.0 버전을 사용하기 위해 GLSurfaceView.setEGLContextClientVersion(2) 를 호출합니다.

GLSurfaceView.setRenderer(Renderer)를 호출하여, GLSurfaceView에 그릴 렌더러를 준비합니다.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        gl_surface_view.setEGLContextClientVersion(2)
        gl_surface_view.setRenderer(MyRenderer())
    }
}

GLSurfaceView.Renderer

Renderer는 범용 렌더러 인터페이스로 프레임을 렌더링 하기 위해 OpenGL 함수를 호출을 수행합니다.

GLSurfaceView와의 연결은 GLSurfaceView.setRenderer(GLSurfaceView.Renderer)를 통해 가능합니다.

1_Triangle 예제에서는 Renderer인터페이스를 구현한 MyRenderer라는 클래스를 만들었었습니다. 렌더러 인터페이스를 구현하게 되면 필수적으로 구현해야할 메소드 3가지가 있습니다.

  • onSurfaceCreated(GL10 gl, EGLConfig config) : 서비스가 생성 또는 재생성될 때 호출됩니다. 
  • onSurfaceChanged(GL10 gl, int width, int height) : surface의 사이즈가 변경될 경우 호출됩니다.
  • onDrawFrame(GL10 gl) : 현재 프레임을 그리기 위해 호출됩니다.

onSurfaceCreated는 렌더링쓰레드가 시작되고 EGL context가 유실될 때마다 호출 됩니다. EGL Context는 일반적으로 sleep모드가 된 후 디바이스가 깨어 날 때 유실됩니다.

onSurfaceChanged에서는 보통 viewport를 지정합니다. 만약 카메라(사진찍는 카메라가 아님) 고정되어있다면 프로젝션 매트릭스를 여기서 설정할 수도 있습니다.

onDrawFrame에서는 화면에 그리고자 하는 내용에 대한작업을 주로 합니다.

이제 삼각형을 그리기 위한 소스를 파악해보도록 하겠습니다.

MyRenderer.kt

화면을 그리는 주된 역할을 하게될 클래스입니다.

GLSurfaceView.Renderer 구현하여 위에서 설명한 3가지 메소드를 구현하게 될것입니다.

import android.opengl.GLES20.*
import android.opengl.GLSurfaceView
import com.charlezz.a01_triangle.GlUtil.checkGlError
import com.charlezz.a01_triangle.GlUtil.createProgram
import java.nio.Buffer
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10

class MyRenderer : GLSurfaceView.Renderer {

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

    val fragmentShader = "" +
            "precision mediump float;" +
            "void main() {" +
            "  gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);" +
            "}"

    val triangleVertices: Buffer = GlUtil.createFloatBuffer(arrayOf(
            0.0f, 0.5f,
            -0.5f, -0.5f,
            0.5f, -0.5f
    ).toFloatArray())

    var vPositionHandle: Int = 0
    var program: Int = 0
    var grey = 0f
    var flag = false
    override fun onDrawFrame(p0: GL10?) {
        if( grey > 1.0f || grey < 0.0f){
            flag = !flag
        }
        if(flag){
            grey += 0.01f
        }else{
            grey -= 0.01f
        }

        glClearColor(grey,grey,grey, 1.0f)
        checkGlError("glClearColor")

        glClear(GL_DEPTH_BUFFER_BIT or GL_COLOR_BUFFER_BIT)
        checkGlError("glClear")

        glUseProgram(program)
        checkGlError("glUseProgram")

        glVertexAttribPointer(vPositionHandle,2,GL_FLOAT, false, 0, triangleVertices)
        checkGlError("glVertexAttribPointer")

        glEnableVertexAttribArray(vPositionHandle)
        checkGlError("glEnableVertexAttribArray")

        glDrawArrays(GL_TRIANGLES, 0, 3)
        checkGlError("glDrawArrays")
    }

    override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) {
        program = createProgram(vertexShader, fragmentShader)

        vPositionHandle = glGetAttribLocation(program, "vPosition")
        checkGlError("glGetAttribLocation")

        glViewport(0, 0, width, height)
        checkGlError("glViewport")
    }

    override fun onSurfaceCreated(gl: GL10, p1: EGLConfig) {
        //nothing to do
    }
}

VertexShader(정점 쉐이더)

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

정점을 계산하기 위한 버텍스 쉐이더 입니다.

  • attribute :  Vertex Shader에서만 사용가능한 타입. 정점 정보를 전달하기위해 사용합니다.
  • vec4 : homogeneous 좌표계의 기본적인 정점 위치를 갖는 4차원(x,y,z,w) float형 벡터 데이터입니다. 지금은 삼각형만 그리므로 평면상의 x축과 y축만 이해하면됩니다.
  • gl_Position : Vertex Shader의 내장변수중 다음 스테이지에 정점의 위치 정보를 전달합니다.

FragmentShader(프레그먼트 쉐이더)

val fragmentShader = "" +
    "precision mediump float;" +
    "void main() {" +
    "  gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);" +
    "}"
  • precision mediump : 정확도를 중간으로 설정합니다. Highp는 최고 정확도, lowp는 최하 정확도입니다. Highp의 경우 디바이스가 지원을 안하는 경우도 있고, 퍼포먼스측면에서 문제가 될수도 있습니다.
  • gl_FragColor : 최종 프래그먼트 색상으로 고정된 내장 변수 입니다.

삼각형 정점 버퍼 만들기

삼각형을 그리기위해 꼭지점 3개를 찍습니다.

val triangleVertices: Buffer = GlUtil.createFloatBuffer(arrayOf(
        0.0f, 0.5f,
        -0.5f, -0.5f,
        0.5f, -0.5f
).toFloatArray())

Note : 안드로이드에서 OpenGL을 위한 버퍼 사용시 주의해야할 점은 자바의 힙메모리는 사용할 수 없다는 점입니다. 그러므로 NIO패키지에서 ByteBuffer.allocate(int)가 아닌 ByteBuffer.allocateDirect(int)를 이용해야 합니다.

onSurfaceCreated

Surface가 생성 또는 재생성 될때 호출되는데 당장은 이번 예제에서는 딱히 할 작업이 없으므로 생략하겠습니다.

onSurfaceChanged

Surface의 사이즈에 변화가 생길 때 호출됩니다. 이곳에서 몇몇 작업을 하도록합니다.

program = createProgram(vertexShader, fragmentShader)
//쉐이더 프로그램을 하나 생성합니다.
//버텍스 쉐이더와 프레그먼트 쉐이더 코드가 필요합니다.

vPositionHandle = glGetAttribLocation(program, "vPosition")
//버텍스 쉐이더에서 선언한 변수 vPosition을 연결할 핸들러를 가져옵니다.

glViewport(0, 0, width, height)
//Surface에 꽉채운 화면을 OpenGL로 구성하기 위해 뷰포트를 Surface 사이즈로 설정합니다.

onDrawFrame

glClearColor(grey,grey,grey, 1.0f) 
//이전에 있던 버퍼를 지우고 RGBA색상으로 배경색을 지정합니다.
glClear(GL_DEPTH_BUFFER_BIT or GL_COLOR_BUFFER_BIT)
//GL_DEPTH_BUFFER_BIT으로 뎁스버퍼를 지웁니다. 
//OpenGL은 3차원 세계를 표현하여 디스플레이의 2차원 평면에 표현하는데,
//픽셀하나하나 뎁스버퍼를 따로 관리합니다.
//동일 좌표상에 있는 겹쳐진 픽셀 또는 물체를 그리고자 할때 이 뎁스버퍼를 이용하는데,
//이전 뎁스 버퍼가 그대로 남아있으면 화면을 그릴때 문제가 되므로 초기화 시킵니다.
//GL_COLOR_BUFFER_BIT은 glClearColor에서 지정한 색으로 배경을 칠합니다.

glUseProgram(program)
//미리 만들어두었던 쉐이더 프로그래을 이용합니다.

glVertexAttribPointer(vPositionHandle,2,GL_FLOAT, false, 0, triangleVertices)
// 버텍스 핸들러를 이용하여 각 정점의 위치를 계산합니다.
// 정점 쉐이더에서 vec4를 이용하였기 때문에 한 정점의 표현에 대해 3차원표현(x,y,z,1)을 할수 있으나,
// 지금은 단순히 2차원 평면상에 삼각형을 그리는것이므로 x,y값만 쓰도록 합니다.
// 그렇기 때문에 float값 2개만 로드하면 되므로 두번째 인자에 2를 넣었습니다.
// 4번째 인자는 고정 소수점 값의 범위를 정규화할지를 결정합니다.(-1에서1 또는 0에서1)
// stride는 일반적으로 정점 배열의 사이즈를 넣습니다 (정점개수 * 4)
// stride값이 0일 경우 배열의 사이즈로 간주합니다.

glEnableVertexAttribArray(vPositionHandle)
// 메모리에 로드한 정점들을 활성화 합니다.

glDrawArrays(GL_TRIANGLES, 0, 3)
// 첫번째 인자는 주어진 정점을 어떤식으로 그릴것인지에 대한것이고,
// 두번째 인자는 메모리에 로드된 정점 배열에서 그리기 시작할 정점
// 마지막 인자는 몇개의 정점을이용하여 화면을 그릴것인지 정합니다.

 

GlUtil.kt

OpenGL 프로그래밍을 하면서 자주 쓰게되는 보일러플레이트 코드를 유틸리티 클래스로 따로 빼둔 것입니다.

import android.opengl.GLES20
import android.util.Log
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer

object GlUtil {
    val TAG = "GlUtil"

    private val SIZEOF_FLOAT = 4

    /**
     * 버텍스 쉐이더와 프레그먼트 쉐이더로 새로운 프로그램을 만듭니다
     * @return 프로그램을 아이디를 리턴합니다. 프로그램을 만드는데 실패할 경우 0을 리턴합니다.
     */
    fun createProgram(vertexSource: String, fragmentSource: String): Int {
        val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource)
        // 버텍스 쉐이더를 로드 합니다.
        if (vertexShader == 0) {
            return 0
        }
        val pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource)
        // 프레그먼트 쉐이더를 로드합니다.
        if (pixelShader == 0) {
            return 0
        }

        var program = GLES20.glCreateProgram() // 빈 쉐이더 프로그램 생성
        checkGlError("glCreateProgram")
        if (program == 0) {
            Log.e(TAG, "Could not create program")
        }
        GLES20.glAttachShader(program, vertexShader)//버텍스 쉐이더를 프로그램에 붙입니다.
        checkGlError("glAttachShader")
        GLES20.glAttachShader(program, pixelShader)//프레그먼트 쉐이더를 프로그램에 붙입니다.
        checkGlError("glAttachShader")
        GLES20.glLinkProgram(program)//쉐이더를 붙였다면 이제 프로그램이 실행가능한 프로그램이 된것입니다.
        val linkStatus = IntArray(1)
        GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0) //프로그램이 잘 연결되었는지 확인
        if (linkStatus[0] != GLES20.GL_TRUE) { //프로그램이 잘 연결되지 않았다면,
            Log.e(TAG, "Could not link program: ")
            Log.e(TAG, GLES20.glGetProgramInfoLog(program)) // 로그
            GLES20.glDeleteProgram(program) // 프로그램 삭제
            program = 0
        }
        return program
    }

    /**
     * 제공된 쉐이더 소스를 컴파일 합니다.
     *
     * @return 쉐이더 아이디를 리턴합니다. 실패시 0을 리턴합니다.
     */
    fun loadShader(shaderType: Int, source: String): Int {
        var shader = GLES20.glCreateShader(shaderType) // 쉐이더 핸들을 만듭니다
        checkGlError("glCreateShader type=$shaderType")
        GLES20.glShaderSource(shader, source) // 쉐이더소스를 연결합니다.
        GLES20.glCompileShader(shader) // 컴파일 합니다.
        val compiled = IntArray(1)
        GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0) // 컴파일에 문제가 없는지 확인합니다
        if (compiled[0] == 0) {
            Log.e(TAG, "Could not compile shader $shaderType:")
            Log.e(TAG, " " + GLES20.glGetShaderInfoLog(shader)) //로그
            GLES20.glDeleteShader(shader) //쉐이더 핸들 삭제
            shader = 0
        }
        return shader
    }

    /**
     * GLError가 발생했는지 확인합니다.
     */
    fun checkGlError(op: String) {
        val error = GLES20.glGetError()
        if (error != GLES20.GL_NO_ERROR) {
            val msg = op + ": glError 0x" + Integer.toHexString(error)
            Log.e(TAG, msg)
            throw RuntimeException(msg)
        }
    }

    /**
     * C++레벨의 float 배열을 저장하기 위한 메모리를 생성합니다.
     */
    fun createFloatBuffer(coords: FloatArray): FloatBuffer {
        // Allocate a direct ByteBuffer, using 4 bytes per float, and copy coords into it.
        val bb = ByteBuffer.allocateDirect(coords.size * SIZEOF_FLOAT) // 반드시 allocateDirect로 생성
        bb.order(ByteOrder.nativeOrder())
        val fb = bb.asFloatBuffer()
        fb.put(coords)
        fb.position(0)
        return fb
    }
}

본 프로젝트는 github에서 확인 가능합니다.

Buy me a coffeeBuy me a coffee
카테고리: AndroidGraphics

1개의 댓글

Jason · 2019년 8월 12일 9:27 오전

잘봤습니다!

답글 남기기

이메일은 공개되지 않습니다.