본문 바로가기
안드로이드 코틀린

[Kotlin][Android] 삼각형 그리기 (OpenGL)

by teamnova 2022. 4. 16.
728x90

안녕하세요. 이번 시간에는 OpenGL을 사용하여 스마트폰 화면 위에 삼각형 렌더링 하는 방법에 대해서 알아보겠습니다.

 

- OpenGL ES2.0을 사용하겠습니다.

우선 Manifest에 사용을 표시해줍니다.

<manifest ...>
...

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

..
</manifest>

 

그 후 시작 화면을 위한 MainActivity를 만들어주고, 그 레이아웃에 GLSurfaceView를 추가합니다.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".DrawActivity">


        <android.opengl.GLSurfaceView
            android:id="@+id/GLSurfaceView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

 

XML에 있는 GLSurfaceView를 MainActivity의 멤버변수 mSurfaceView로 참조합니다.

이후, GLSurfaceView를 사용하기 위하여 필요한 초기화를 해줍니다.

 

setELContextClientVersion(); 함수를  사용하여, 어떤 openGL버전을 사용할지 정해줍니다.

그 GLSurface의 Renderer 방식을 연결하면 됩니다. 초기화는 이 정도로 끝내고 Renderer 클래스를 하나 더 만들어줍시다.

 

 

MainActivity.kt 

class MainActivity : AppCompatActivity() {


    private lateinit var mSurfaceView: GLSurfaceView
    private lateinit var renderer : Renderer

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.draw_activity);
        //binding = DataBindingUtil.setContentView(this, R.layout.draw_activity);

        mSurfaceView = findViewById(R.id.GLSurfaceView)// binding.GLSurfaceView
        initialSurface();

    }
    
    fun initialSurface() {
        mSurfaceView.setEGLContextClientVersion(2); // OpenGL ES 2.0 버전을 사용함.
        renderer = Renderer();
        mSurfaceView.setRenderer(renderer);

    }
}

 

Renderer 클래스를 생성합니다. GLSurfaceView에 표시하기 위한 Renderer을 만들고 있으므로, GLSurfaceView.Renderer 인터페이스를 상속해줍니다.

 

GLSurfaceView.Renderer 인터페이스를 상속하게 되면, 세 가지 함수를 구현해야 합니다.

- onSurfaceCretaed()

- onSurfaceChanged()

- onDrawFrame()

 

onSurfaceCreated는 처음에 Surface가 생성될 때, 사용됩니다. 여기서 배경색을 설정해줍니다.

onSurfaceChanged는 화면 모드 변경 및 Surface사이즈가 변경되었을 때 호출됩니다.

onDrawFrame()는 매번 프레임을 갱신할 때마다 사용됩니다.

 

class Renderer : GLSurfaceView.Renderer {
    private lateinit var mTriangle : Triangle;
    
      override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
        // 배경색 설정
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

        // 트라이앵글을 초기화
        mTriangle = Triangle();
    }

    override fun onSurfaceChanged(p0: GL10?, p1: Int, p2: Int) {

        // 화면에 그림이 그려지는 면적을 설정함
        GLES20.glViewport(0, 0, p1, p2)

    }

    override fun onDrawFrame(p0: GL10?) {
    	// 다시 그릴 때 배경색
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);     
    }
}

 

저희는 삼각형을 그릴 예정이므로, 삼각형 클래스를 하나 더 만들어줍니다.

COORDS_PER_VERTEX는 현재 배열에서 꼭지점 당 vertex수를 나타냅니다.

그리고 삼각형 세 점의 위치를 담은 배열을 선언합니다.

vertex 사이를 채울 색깔을 결정합니다.

 

Triangle.kt

class Triangle {
    private val COORDS_PER_VERTEX = 3;
    
    private var triangleCoords: FloatArray = arrayOf(
        0.0f, 0.5f, 0.0f, // 꼭대기
        -0.5f, -0.5f, 0.0f, // 바닥 왼쪽
        0.5f, -0.5f, 0.0f // 바닥 오른쪽
    ).toFloatArray();
    
	private val color : FloatArray = arrayOf(1.0f, 1.0f, 1.0f, 1.0f).toFloatArray();

}

 

초기화 코드를 작성합니다.

추후 삼각형을 그릴 때 GL프로그램에 삼각형 정점의 위치를 전달해야 합니다. Shader 프로그램에는 버퍼를 통해 값을 전달할 수 있습니다. 

FloatBuffer에 메모리를 할당하고, 삼각형의 좌표를 대입합니다.

 

Traingle.kt

private var vertexBuffer: FloatBuffer

init {
        // 모양의 좌표를 위해 byteBuffer을 초기화시킴
        // 좌표 갯수 * 4 (float 하나당 4byte)
        var byteBuffer : ByteBuffer = ByteBuffer.allocateDirect(triangleCoords.size * 4);

        // 디바이스 하드웨어의 native byte 순서로 바꿈.
        byteBuffer.order(ByteOrder.nativeOrder());

        // 생성한 바이트 버퍼를 float버퍼로 치환하여, vertexBuffer 멤버변수에 floatBuffer을 할당함.
        vertexBuffer = byteBuffer.asFloatBuffer();

        // vertexBuffer에 traingle 좌표를 대입함.
        vertexBuffer.put(triangleCoords);
        
        // 버퍼의 읽기 포인트를 맨 처음으로 옮김
        vertexBuffer.position(0);
    }

 

OpenGL ES는 C++ 셰이더를 사용하여 그래픽 렌더링을 처리합니다.

따라서 C++ 코드를 셰이더에 전달할 문자열을 설정합니다.

이후 셰이더 소스코드를 불러와서 vertexShader와 fragmentShader을 만들어줍니다.

프로그램 생성 후에 해당 프로그램에 셰이더를 연결해줍니다.

마지막으로 프로그램을 실행합니다.

 

쉐이더 소스코드를 로드하고 컴파일 하는 부분은 vertexShader과 fragmentShader 양쪽 모두 사용되므로 코드 중복을 막기 위하여 함수를 분리해줍니다.

 

프로그램을 실행해야하므로, 프로그램을 참조하는 멤버변수 mProgram을 앞에 선언해둡니다.

 

Triangle.kt

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

    val FRAGMENT_SHADER_CODE = "precision mediump float;" +
            "uniform vec4 vColor;" +
            "void main(){" +
            "gl_FragColor = vColor;" +
            "}"
            
   private var mProgram : Int = -1;


   init {            
    	/... 생략 ..../
    
      val vertexShader : Int = Renderer.loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_CODE);
      val fragmentShader : Int = Renderer.loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_CODE);

      mProgram = GLES20.glCreateProgram();

      //  vertexShader, fragmentShader을 프로그램에 장착
      GLES20.glAttachShader(mProgram, vertexShader);
      GLES20.glAttachShader(mProgram, fragmentShader);

      // 실행
      GLES20.glLinkProgram(mProgram);
   }
    
    fun loadShader(type : Int, shaderCode : String) : Int {

        val shader = GLES20.glCreateShader(type);
        GLES20.glShaderSource(shader, shaderCode);
        GLES20.glCompileShader(shader);

    	return shader;
    }

 

 

사전 준비는 끝났으니 삼각형을 그려보겠습니다.

VertexShader부분의 vPosition에 vertexBuffer 데이터를 넣어주고, fragmentShader부분의 vColor에 색상 데이터인 color을 넣어줘야 합니다. 이를 위하여 vPosition과 vColor를 위한 핸들러를 만들어줍시다.

이어서 draw 부분을 작성합니다.

 

우선 삼각형 생성 당시, 만들었던 프로그램을 사용해야합니다. glProgram(mProgram);

그리고 vPosition과 vColor의 멤버를 핸들링 하기 위해 멤버변수로 참조해줍니다.

이후, 각각 핸들러에 vertexBuffer의 데이터와 color 데이터를 넣어줍니다.

최종적으로 삼각형을 그립니다.

 

Triangle.kt

    private var positionHandle : Int = -1;
    private var colorHandle: Int = -1
    
    
    fun draw() {
        
        // OpenGL ES 환경에 프로그램을 추가함
        GLES20.glUseProgram(mProgram);

        // vertex Shader의 vPosition memeber을 핸들링함.
        positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

        // 삼각형 정점에 대한 핸들링 활성화
        GLES20.glEnableVertexAttribArray(positionHandle)
        GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, vertexBuffer);

        colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");

        // 삼각형의 색상을 채움.
        GLES20.glUniform4fv(colorHandle, 1, color, 0);

        // 삼각형 그리기
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, triangleCoords.size / COORDS_PER_VERTEX);
        
                
        // vertex handle 해제
        GLES20.glDisableVertexAttribArray(positionHandle);

        // color handle 해제
        GLES20.glDisableVertexAttribArray(colorHandle);
        
   }

 

이제 Renderer에서 삼각형을 그릴 수 있게 추가해야 합니다.

onSurfaceCreated할 때, 삼각형을 생성하고, Frame을 그릴 때마다 삼각형을 그리면 됩니다.

 

Renderer.kt

private lateinit var mTriangle : Triangle;

override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
   // 배경색 설정
   GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

   // 트라이앵글을 초기화
   mTriangle = Triangle();
}

override fun onSurfaceChanged(p0: GL10?, p1: Int, p2: Int) {

   // view의 viewport 사이즈 설정
   GLES20.glViewport(0, 0, p1, p2)
}

override fun onDrawFrame(p0: GL10?) {
   GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
   mTriangle.draw();
}

 

삼각형

 

그리고 어플을 실행하시면, 화면에 삼각형이 렌더링 되는 것을 보실 수 있습니다.

 

 

openGL을 사용하다보면 써야 할 코드가 많습니다.

https://stickode.com/detail.html?no=2869 

 

https://stickode.com/detail.html?no=2869

 

stickode.com

Shader Code 및, 렌더링, 프로그램 생성 등을 미리 스틱코드에 저장해두시고 사용하신다면 훨씬 더 쉽고 편하게 사용할 수 있습니다.