Android OpenGL ES 3.0 Triangle 삼각형 그리기(java 버전)
예전에 윈도우에서 테스트용으로 OpenGL로 몇 가지 만들어 본적이 있었는데 Android에서도 구현이 가능하다고 하여 간략하게 OpenGL의 구성과 사용법을 알아보기 위해 테스트를 진행하게 되었습니다.
Android에서의 OpenGL을 구현하는 방법에 대해서는 안드로이드 개발자 사이트에 설명을 해놓은 자료를 읽어보면 이해하는데 조금 도움이 됩니다.
아래에는 사이트에서 설명하고 있는 내용중 간략하게 중요하다고 생각되는 부분만 발취해 보았고 전체 내용은 아래 링크에서 읽어보는 것이 좋을 것 같습니다.
https://developer.android.com/guide/topics/graphics/opengl?hl=ko
· OpenGL ES 1.0 및 1.1 - 이 API 사양은 Android 1.0 이상에서 지원됩니다. · OpenGL ES 2.0 - 이 API 사양은 Android 2.2(API 레벨 8) 이상에서 지원됩니다. · OpenGL ES 3.0 - 이 API 사양은 Android 4.3(API 레벨 18) 이상에서 지원됩니다. · OpenGL ES 3.1 - 이 API 사양은 Android 5.0(API 레벨 21) 이상에서 지원됩니다. |
OpenGL ES 버전별로 지원하는 안드로이드 버전에 차이가 있는데 저는 OpenGL ES 3.0을 기준으로 작업을 진행 합니다. 프로젝트를 생성할 때 버전에 맞추어 선택을 합니다.
가장 기본이 되는 클래스가 2개가 있는데 하나는 GLSurfaceView로
GLSurfaceView 이 클래스는 OpenGL API 호출을 사용하여 객체를 그리고 조작할 수 있는 View이며 SurfaceView와 기능이 비슷합니다. GLSurfaceView의 인스턴스를 만들고 여기에 Renderer를 추가하여 이 클래스를 사용할 수 있습니다. 그러나 터치 스크린 이벤트를 캡처하려면 OpenGL 학습 과정 터치 이벤트에 응답에 표시된 대로 GLSurfaceView 클래스를 확장하여 터치 리스너를 구현해야 합니다. |
GLSurfaceView는 화면(View)를 생성하는 클래스로 그려진 이미지를 표현하게 되고 화면 터치 이벤트는 직접 구현해야 한다고 되어있습니다.
OpenGL에서는 GLSurfaceView를 Object로 생성하여 Android의 Layout 대신 사용합니다. activity_main.xml를 사용하는 방법도 있는지 좀더 학습을 해봐야겠지만 GLSurfaceView를 사용합니다.
두번째로 렌더링 관련 클래스입니다.
GLSurfaceView.Renderer 이 인터페이스를 통해서는 GLSurfaceView에 그래픽을 그리는 데 필요한 메서드를 정의합니다. 개별 클래스로 구현된 이 인터페이스를 제공하고 GLSurfaceView.setRenderer()를 사용하여 GLSurfaceView 인스턴스에 연결해야 합니다. GLSurfaceView.Renderer 인터페이스를 사용하려면 다음 메서드를 구현해야 합니다.
|
GLSurfaceView.Renderer가 실제 이미지를 그리는 클랙스이고 위에 있는 3개의 메서드를 이용하여 생성과 화면의 변경사항에 따른 이미지 변경 작업등을 하게 됩니다.
OpenGL ES는 현재 3.1까지 진행이 되고 있는데 어떤 버전을 사용하여 작업하는지에 대한 설정을 AndroidManifest.xml 파일에 설정을 합니다.
<!-- Tell the system this app requires OpenGL ES 3.1. --> <uses-feature android:glEsVersion="0x00030001" android:required="true" /> |
위에 표시한 예시 3개중 사용하려는 OpenGL ES 버전에 맞추어 선택해서 하나만 입력합니다.
OpenGL ES API 버전 선택을 위한 참고사항으로 다음과 같은 글이 있습니다.
OpenGL ES 1.0/1.1 API와 OpenGL ES 2.0 및 3.0의 프로그래밍 방식은 상당히 다르므로 개발자는 이 API를 사용하여 개발을 시작하기 전에 다음 요소를 신중하게 고려해야 합니다. |
버전별로 구현하는 방법이 다르므로 버전선택을 잘 해야 합니다.
지금까지 Google에서 OpenGL을 Android에 적용하기위해 필요한 사항을 정리한 페이지를 간략하게 알아보았습니다.
프로그래머는 실제 코딩을 봐야 이해가 되어서 샘플 프로젝트를 시작해 보도록 하겠습니다.
참고하려는 소스는 구글 개발자 사이트에서 제공하는 소스를 선택했습니다.
원본 내용을 보려면 설명과 함께 잘 정리되어있으니 한번 방문해서 스터디하시기 바랍니다.
https://developer.android.com/develop/ui/views/graphics/opengl/environment?hl=ko
안드로이드 버전은 Giraffe 2022.3.1으로 작업했습니다.
1. 프로젝트 생성
오늘 구현하려는 결과 이미지입니다.
앱을 만들기 위해 프로젝트를 하나 생성합니다.
먼저 [Empty Views Activity]를 선택한다. 예전 Android Studio 버전처럼 Empty Activity를 선택하면 언어 선택 항목이 없어서 java로 개발할 수 없습니다.
프로젝트 Name은 적당하게 기입하고 나머지 항목은 OpenGL ES 3.0버전에 맞추어 선택합니다.
Minimum SDK를 잘 선택하면 되겠네요.
프로젝트가 생성되면 아무것도 수정하지 않은 상태에서 실행을 해서 오류가 나는지 확인해 봅니다.
기본화면이 이상없이 잘 나오는군요. 가끔 업데이트를 하다 보면 초기 설정이 적절하지 못해 오류가 발생하여 버그 수정하는 시간이 아깝더군요.
이제 코딩을 시작 합니다.
AndroidManifest.xml 파일에 OpenGL ES 3.0을 사용하기 위한 설정을 진행합니다.
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
프로젝트 생성시 Minimum SDK를 선택한 것에 맞추어 OpenGL 3.0 사용 설정을 합니다.
2. 이미지 그리기
먼저 그리고자 하는 대상을 선택 하겠습니다. 세모, 네모가 제일 간단한데 그중에 좀더 간단할것 같아 보이는 세모를 정의해 보겠습니다.
Google에 있는 설명을 그대로 이용해서 작성을 하려고 하는데 사이트에는 설명을 위해 프로그램을 조각내서 이곳 저곳에 설명을 해놓았는데 한번 붙여서 구현을 해봅니다.
원점을 기준으로 삼각형의 좌표상 위치를 설정합니다. 구글 사이트에 있는 삼각형 정의입니다.
static float triangleCoords[] = { // in counterclockwise order:
0.0f, 0.622008459f, 0.0f, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
};
상단은 y 축만 값을 가지면 되고 좌우측 좌표 점 값으로 삼각형을 정의합니다.
public Triangle() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (number of coordinate values * 4 bytes per float)
triangleCoords.length * 4);
// use the device hardware's native byte order
bb.order(ByteOrder.nativeOrder());
// create a floating point buffer from the ByteBuffer
vertexBuffer = bb.asFloatBuffer();
// add the coordinates to the FloatBuffer
vertexBuffer.put(triangleCoords);
// set the buffer to read the first coordinate
vertexBuffer.position(0);
좌표점을 이용하여 삼각형 꼭지점을 정의합니다.
꼭지점을 이용하여 삼각형을 그립니다.
public void draw() {
// Add program to OpenGL ES environment
GLES30.glUseProgram(mProgram);
// get handle to vertex shader's vPosition member
positionHandle = GLES30.glGetAttribLocation(mProgram, "vPosition");
// Enable a handle to the triangle vertices
GLES30.glEnableVertexAttribArray(positionHandle);
// Prepare the triangle coordinate data
GLES30.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,
GLES30.GL_FLOAT, false,
vertexStride, vertexBuffer);
// get handle to fragment shader's vColor member
colorHandle = GLES30.glGetUniformLocation(mProgram, "vColor");
// Set color for drawing the triangle
GLES30.glUniform4fv(colorHandle, 1, color, 0);
// Draw the triangle
GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES30.glDisableVertexAttribArray(positionHandle);
}
Google에서는 OpenGL ES 2.0으로 소스를 설명을 하고 있는데 3.0으로 변경을 해주었습니다.
3. 도형 렌더링
삼각형을 그리긴 했지만 Surface에 표현하는 작업들이 남았습니다.
위에서 설명한 2개의 기본 클래스인 Surface를 정의하고 Rendering 을 하는 두개의 프로그램이 추가되어야 합니다.
먼저 Renderer 파일을 생성합니다.
Google에 있는대로 MyGLRenderer.java 파일을 생성하고
필수 메소드를 추가해 줍니다.
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// Set the background frame color
GLES30.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
// initialize a triangle
mTriangle = new Triangle();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES30.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
// Redraw background color
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
}
추가한 메소드에 코드를 추가합니다.
onSurfaceCreated() - 뷰의 OpenGL ES 환경을 설정하기 위해 한 번 호출됩니다.
초기값을 설정해야 하면 여기에 정의합니다.
onDrawFrame() - 뷰를 다시 그릴 때마다 호출됩니다.
onSurfaceChanged() - 기기의 화면 방향이 변경되는 경우와 같이 뷰의 도형이 변경되면 호출됩니다.
이제 Surface 파일을 작성합니다.
MyGLSurfaceView.java 파일을 생성하고 GLSurfaceView로부터 상속을 받습니다.
그리고 생성자를 추가하는데 첫번째 생성자를 선택 합니다.
private final MyGLRenderer renderer;
public MyGLSurfaceView(Context context) {
super(context);
// Create an OpenGL ES 2.0 context
setEGLContextClientVersion(2);
renderer = new MyGLRenderer();
// Set the Renderer for drawing on the GLSurfaceView
setRenderer(renderer);
}
소스는 간단합니다.
생성한 삼격형 렌더링을 SurfaceView에 넣어주면 됩니다.
4. 프로젝트 실행
이제 모든 작업이 완료되었으니 MainActivity에서 생성시 layout에 지금까지의 작업을 실행하기만 하면 되겠네요.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main);
// Create a GLSurfaceView instance and set it
// as the ContentView for this Activity.
gLView = new MyGLSurfaceView(this);
setContentView(gLView);
}
MainActivity 작업이 제일 간단 합니다.
Android layout을 사용하지 않고 OpenGL에서 생성하여 사용한다고 설명하였듯이 새로 생성한 SerfaceView를 추가해주면 끝입니다.
구글에 있는 소스가 너무 나뉘어있고 이곳저곳에서 생략된 표시도 있어서 작동을 하기는 할까? 하는 걱정이 있었는데 위에 있는 소스를 실행하면 좀 단순 하지만 결과가 나옵니다.
Layout이나 Gradle에 추가한 작업은 없고 그냥 3개의 신규 파일이 추가되고 2개의 파일(AndroidManifest.xml, MainActivity.java)에 몇 줄 소스 작업만 진행하였습니다.
5. 전체 소스
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.OpenGLTriangle"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
MainActivity.java
package com.copycoding.opengltriangle;
import androidx.appcompat.app.AppCompatActivity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
private GLSurfaceView gLView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main);
// Create a GLSurfaceView instance and set it
// as the ContentView for this Activity.
gLView = new MyGLSurfaceView(this);
setContentView(gLView);
}
}
MyGLRenderer.java
package com.copycoding.opengltriangle;
import android.opengl.GLES30;
import android.opengl.GLSurfaceView;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class MyGLRenderer implements GLSurfaceView.Renderer {
private Triangle mTriangle;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// Set the background frame color
GLES30.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
// initialize a triangle
mTriangle = new Triangle();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES30.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
// Redraw background color
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
mTriangle.draw();
}
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 = GLES30.glCreateShader(type);
// add the source code to the shader and compile it
GLES30.glShaderSource(shader, shaderCode);
GLES30.glCompileShader(shader);
return shader;
}
}
MyGLSurfaceView.java
package com.copycoding.opengltriangle;
import android.content.Context;
import android.opengl.GLSurfaceView;
public class MyGLSurfaceView extends GLSurfaceView {
private final MyGLRenderer renderer;
public MyGLSurfaceView(Context context) {
super(context);
// Create an OpenGL ES 2.0 context
setEGLContextClientVersion(2);
renderer = new MyGLRenderer();
// Set the Renderer for drawing on the GLSurfaceView
setRenderer(renderer);
}
}
Triangle.java
package com.copycoding.opengltriangle;
import android.opengl.GLES30;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
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;" +
"}";
private FloatBuffer vertexBuffer;
// number of coordinates per vertex in this array
static final int COORDS_PER_VERTEX = 3;
static float triangleCoords[] = { // in counterclockwise order:
0.0f, 0.622008459f, 0.0f, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
};
// Set color with red, green, blue and alpha (opacity) values
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
private final int mProgram;
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 Triangle() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (number of coordinate values * 4 bytes per float)
triangleCoords.length * 4);
// use the device hardware's native byte order
bb.order(ByteOrder.nativeOrder());
// create a floating point buffer from the ByteBuffer
vertexBuffer = bb.asFloatBuffer();
// add the coordinates to the FloatBuffer
vertexBuffer.put(triangleCoords);
// set the buffer to read the first coordinate
vertexBuffer.position(0);
int vertexShader = MyGLRenderer.loadShader(GLES30.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = MyGLRenderer.loadShader(GLES30.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// create empty OpenGL ES Program
mProgram = GLES30.glCreateProgram();
// add the vertex shader to program
GLES30.glAttachShader(mProgram, vertexShader);
// add the fragment shader to program
GLES30.glAttachShader(mProgram, fragmentShader);
// creates OpenGL ES program executables
GLES30.glLinkProgram(mProgram);
}
public void draw() {
// Add program to OpenGL ES environment
GLES30.glUseProgram(mProgram);
// get handle to vertex shader's vPosition member
positionHandle = GLES30.glGetAttribLocation(mProgram, "vPosition");
// Enable a handle to the triangle vertices
GLES30.glEnableVertexAttribArray(positionHandle);
// Prepare the triangle coordinate data
GLES30.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,
GLES30.GL_FLOAT, false,
vertexStride, vertexBuffer);
// get handle to fragment shader's vColor member
colorHandle = GLES30.glGetUniformLocation(mProgram, "vColor");
// Set color for drawing the triangle
GLES30.glUniform4fv(colorHandle, 1, color, 0);
// Draw the triangle
GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES30.glDisableVertexAttribArray(positionHandle);
}
}
Google 사이트에 있는 소스를 복붙하다보니 띄어쓰기 줄맞춤은 좀보기 흉하네요.
Android에서 OpenGL을 어떻게 사용하는가에 대한 흐름을 대충 알것 같습니다.
몇개 더 작업을 하면서 학습을 해봐야 겠네요.
- copy coding -