본문 바로가기
Opengles 3.0 with Android

Chapter 1.0 NDK 설정 및 초기 코드 작성

by SimonLee 2025. 2. 21.

시작하면서

OpenGL ES3.0 Cookbook 예제를 중심으로 설명한다.

코드가 오래전에 작성되어 현재 안드로이드 스튜디오에서 빌드가 되지 않는 부분들을 수정하고,

덜 중요한 부분을 제거하고, 필요한 부분들로 구성하여 블로그를 정리했습니다.

 

이 강의를 만든 이유는

알고리즘 이론 강의는 많지만, 실제 구현 관련된 강의들은 기본적인 내용들만 담거나 스킵하는 부분들이 많아서

게시물을 작성하게 되었습니다.

이 게시물의 독자들은 OpenGL ES2.0과 그래픽스 이론을 이미 어느정도 알고 있다고 가정하고

진행합니다.

 

 

강의 예제는 아래에서 다운로드 받을 수 있습니다.

- https://github.com/dlgmlals3/AndroidApps/tree/main/1.OpenGLES3.0

 

AndroidApps/1.OpenGLES3.0 at main · dlgmlals3/AndroidApps

AndroidApps. Contribute to dlgmlals3/AndroidApps development by creating an account on GitHub.

github.com

 

위의 코드는 Android 3d application 코드와 필요한 라이브러리 (glm, texture, mesh loader, 등) 이 포함이 되어있습니다.

3d app의 경우 외부 라이브러리를 대부분 사용합니다.

강의 진행은  예제 실습 중심으로 진행이 될것이며, 3d app 구현 관점에서 진행 될 것입니다.

 

안드로이드 시스템에서 OpenGLES를 사용하기 위해서는

안드로이드 View 렌더링의 대해서 기초 코드를 알아야 합니다.

 

 

코드를 다운로드 받으면 이런식으로 폴더가 구성이 되어 있습니다.

 

 

액티비티 파일입니다.

GLESAcvitiy 파일에서 Activity를 상속받고,

onCreate, onPause, onResume오버라이딩 합니다.

onCreate 함수에서는 GLESView 객체를 생성하고 파라메터로 Context를 넘겨줍니다.

setContentView 함수에서 GLESView를 파라메터로 넘겨 주면,

View에서 렌더링되는 내용이 화면에 출력됩니다.

public class GLESActivity extends Activity {

    GLESView mView;

    @Override protected void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        mView = new GLESView(getApplication());
        setContentView(mView);
    }

    @Override protected void onPause() {
        super.onPause();
        mView.onPause();
    }

    @Override protected void onResume() {
        super.onResume();
        mView.onResume();
    }
}

 

OpenGLES api 사용하기 위해서는 EGL라이브러리를 사용 합니다.

1. EGL Context를 생성해야 합니다.

    -GLSurfaceView.EGLContextFactory 클래스를 사용

 

2. EGL Config를 설정해야 합니다.

  - 코드에서는 rgba8888, 뎁스, 스텐실을 사용하도록 되어있습니다.

  - GLSurfaceView.EGLConfigChooser interface 사용

3. 렌더러를 설정해야 합니다.

- setRenderer 함수에 파라메터로 GLSurfaceView.Renderer 클래스를 구현한 객체를 넣어줘야 합니다.

 

렌더러는 3개의 함수를 오버라이드 합니다.

- onDrawFrame 함수는 매프레임마다 호출

- onSurfaceChanged 함수는 서피스 해상도가 변경될때 호출

- onSurfaceCreated서피스가 생성될때 호출합니다.

class GLESView extends GLSurfaceView {
    private static String TAG = "GL2JNIView";
    private static final boolean DEBUG = false;

    public GLESView(Context context) {
        super(context);
        init(false, 0, 0, context.getPackageResourcePath());
    }

    public GLESView(Context context, boolean translucent, int depth, int stencil) {
        super(context);
        init(translucent, depth, stencil, context.getPackageResourcePath());
    }
    
    private void init(boolean translucent, int depth, int stencil, String filePathArg) {
        setEGLContextFactory(new ContextFactory());

        setEGLConfigChooser( translucent ?
                new ConfigChooser(8, 8, 8, 8, depth, stencil) :
                new ConfigChooser(5, 6, 5, 0, depth, stencil) );

        /* Set the renderer responsible for frame rendering */
        Renderer rendererObj = new Renderer();
        rendererObj.filePath = filePathArg;
        setRenderer(rendererObj);
    }
	...
    
    private static class Renderer implements GLSurfaceView.Renderer {
        public String filePath;

        public void onDrawFrame(GL10 gl) {
            GLESNativeLib.step();
        }

        public void onSurfaceChanged(GL10 gl, int width, int height) {
            Log.w(TAG, "OSC");
            GLESNativeLib.resize(width, height);
        }

        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            GLESNativeLib.init(filePath);
        }
    }
}

 

JNI를 통하여 안드로이드 앱에서 네이티브까지 함수 호출이 되도록 합니다.

JNI 함수 명은 Java_패키지명_클래스이름_함수명 입니다.

 

GLESNativeLib 에서 glNative 라이브러리를 로딩하는데.

이 라이브러리는 네이티브 파일을 빌드한 라이브러리 이다. (cpp 폴더)

package cookbook.gles;

public class GLESNativeLib {

    static {
        System.loadLibrary("glNative");
    }

    /**
     * @param width the current opengl view window width
     * @param height the current opengl view window view height
     */
    public static native void init( String apkFilePath );
    public static native void resize(int width, int height );
    public static native void step();

    public static native void TouchEventStart( float x, float y );
    public static native void TouchEventMove( float x, float y );
    public static native void TouchEventRelease( float x, float y );

}

 

자바 파일이 cpp 연결되는 부분은 NativeTemplate.cpp, NativeTemplate.h 파일을 보면 된다.

 

 

아래 내용은 JNI을 생성하는 부분이다. 참고하자.

==================================================================================

==================================================================================

==================================================================================

1. 초기 설정

안드로이드 스튜디오 Tools -> SDK Manager -> SDK Tools 탭 -> NDK 체크 하고 설치한다.

 

File -> Project Strucutre -> SDK Location -> NDK Location 설정이 되어야 하는데

local.properties 파일에서 ndk.dir을 입력해주면 자동으로 설정된다.

 

2. 자바에서 생성한 JNI를 연결

아래에서 생성할 jni 라이브러리 명을 System.loadLibrary( ) 파라메터로 넣는다.

jni를 사용하여 주고받을 함수를 정의를 한다.

public static native <리턴타입 > 함수 형식으로 정의한다.

package com.example.init;

public class GLESNativeLib {

    static {
        try {
            System.loadLibrary("glNative");
        } catch (UnsatisfiedLinkError e) {
            System.err.println("Native code library failed to load.\n" + e);
            System.exit(1);
        }
    }
    public int aaa() { return 0;}
    public int k = 50;
    /**
     * @param width the current opengl view window width
     * @param height the current opengl view window view height
     */

    public static native void init( String apkFilePath );
    public static native void resize(int width, int height );
    public static native void step();
}

 

 

3. JNI Native 코드 생성

JNI 폴더위치상 탐색이 안되므로 

뷰 방식으로 android ->Project 로 변경하자.

 

JNI 폴더 생성은 마우스 우클릭 NEW -> Folder -> JNI 폴더

main 폴더 내부에 jni 폴더가 생성된다.

 

네티티브 코드를 빌드를 하는 방식은 Andorid.mk 방식이 있고, Cmake 방식이 있습니다.

Cmake 방식의 경우에는 new project시 안드로이드 native 샘플 코드를 참조하시면 됩니다.

 

우리는 Android.mk 방식으로 진행 합니다.

먼저 프로젝트 뷰를 Andoid -> Project 로 바꿉니다.

 

src/main/jni 폴더를 생성하고 아래 코드를 넣습니다. 

 

> Cpp 코드 작성

 

c++ 함수와 헤더를 쉽게 만들기 위해서 javac.h를 사용하면 된다.

javac < Java File > -h < 생성될 Path >

 

 

Java File은 native 함수가 정의되어 있는 자바 파일, 필자는 GLESNativeLib 파일을 입력으로 넣어준다.

생성된 파일은 아래와 같다. 

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_init_GLESNativeLib */

#ifndef _Included_com_example_init_GLESNativeLib
#define _Included_com_example_init_GLESNativeLib
#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT void JNICALL Java_com_example_init_GLESNativeLib_init
  (JNIEnv *, jclass, jstring);

JNIEXPORT void JNICALL Java_com_example_init_GLESNativeLib_resize
  (JNIEnv *, jclass, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

생성된 파일을 사용하여 jni cpp, header 파일을 만들어 준다.

 

Cpp 파일 정의 부분

#ifdef __ANDROID__
extern "C" JNIEXPORT void JNICALL Java_com_example_init_GLESNativeLib_init( JNIEnv *env, jobject obj, jstring FilePath )
{
}

extern "C" JNIEXPORT void JNICALL Java_com_example_init_GLESNativeLib_resize( JNIEnv *env, jobject obj, jint width, jint height)
{
}
#endif

 

헤더 파일

#include <jni.h>
#include <android/log.h>
#include <GLES3/gl3.h>
#include <GLES3/gl3ext.h>

/* Header for class com_example_init_GLESNativeLib */
#ifndef _Included_com_example_init_GLESNativeLib
#define _Included_com_example_init_GLESNativeLib

extern "C" {
    JNIEXPORT void JNICALL Java_com_example_init_GLESNativeLib_init(JNIEnv *, jobject, jstring);
    JNIEXPORT void JNICALL Java_com_example_init_GLESNativeLib_resize(JNIEnv *, jobject, jint, jint);
    JNIEXPORT void JNICALL Java_com_example_init_GLESNativeLib_step(JNIEnv *, jobject);
}
#endif

 

생성된 코드는 함수의 두번째 인자 타입이 jclass로 되어있는데, jobject로 바꾸어준다.

 

함수명 해석 방법.

- 빨간 영역은 고정이다.

- <리턴 타입> JAVA_ < Package Name > _ < 해당 Jni를 사용하 Class Name > _ 함수이름

extern "C" 는 외부에서 사용한다는 의미이기 때문에 반드시 써준다.

 

파라메터 해석 방법

첫 번째, 두 번째 인수 JNIEnv, jobject는 모든 JNI 함수에 생기며,

세 번째 인수부터 우리가 전달하고자 하는 인수이다.

세 번째 이후부터는 전달하고자 하는 변수 앞에 j 를 붙여서 보낸다.

 

첫 번째 인수 JNIEnv는 타입 변환할때 사용한다.

대표 적으로 java string을 c 기반 문자열 배열로 바꾸어주는 역할을 한다.

두 번째 인수는 첫 번째 인수와 함께 자바쪽 콜백함수를 만들어줄때 사용한다.

 

첫 번째 인수와 두번째 인수를 가지고 주로 많이 사용하는 기능 2가지를 살펴본다.

첫 번째는 jstring -> char *로 바꾸는 코드이며,

두 번째는 네이티브에서 자바쪽 콜백 메서드를 실행하는 기능이다.

 

 

package com.example.init;

import android.util.Log;

public class GLESNativeLib {

    static {
        try {
            System.loadLibrary("glNative");
        } catch (UnsatisfiedLinkError e) {
            System.err.println("Native code library failed to load.\n" + e);
            System.exit(1);
        }
    }
    public static void onCallBack() {
        Log.d("GLESNativeLib", "onCallbacak");
    }

    /**
     * @param width the current opengl view window width
     * @param height the current opengl view window view height
     */

    public static native void init( String apkFilePath );
    public static native void resize(int width, int height );
    public static native void step();
    public static native String dlgmlals3(String str);
}

 

string을 변환하는 코드는 GetStringUTFChars 함수를 사용하면 된다.

 

콜백 함수의 관련해서는.

클래스를 find 해서 가져오는 방법도 있지만, 두번째 인자로 클래스가 넘어오는 것을 활용해서

호출한 클래스의 콜백함수를 호출해 볼 것이다.

 

GLESNativeLib 클래스를 객체를 만들지 않고, 정적 메서드로 실행하기 때문에 

아래 JNI 함수의 두번째 파라메터가 jobject 가 아니고 jclass 이다.

인스턴스 메서드가 아니므로, 콜백함수도 정적 메서드로 선언해야 한다. (onCallback)

 

CallVoidMethod는 콜백함수가 인스턴스 메서드일때 사용하고,

정적 메서드일 경우, CallStaticVoidMethod를 사용한다.

 

GetStaticMethodID의 세번째 메서드 "()V" 는

괄호 안은 input parameter를 의미하여, V는 리턴타입이 없다는 것을 의미한다.

함수 원형의 따라 달라지는데. 이에 관련해서는 아래 예제를 참고해보자.

https://gist.github.com/bitsnaps/3cfdcf92f6dfef952fbfc5f50c4713b6

 

Simple example using JNI with MinGW C++

Simple example using JNI with MinGW C++. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

 

extern "C" JNIEXPORT jstring JNICALL Java_com_example_init_GLESNativeLib_dlgmlals3(JNIEnv * env, jclass  clazz, jstring dlgmlals3)
{
    //GraphicsRender();
    // string
    const char* w_buf = env->GetStringUTFChars(dlgmlals3, 0);
    __android_log_print(ANDROID_LOG_INFO, "TESTJNI"," String: %s", w_buf);

    // callback
    jmethodID mCallbackB = env->GetStaticMethodID(clazz, "onCallBack", "()V");  // 정적 메서드 ID 획득
    env->CallStaticVoidMethod(clazz, mCallbackB);  // 정적 메서드 onCallBack 호출

    if (env->ExceptionCheck()) {
        env->ExceptionDescribe();  // 예외 내용 출력
        env->ExceptionClear();  // 예외 클리어
    }

    env->ReleaseStringUTFChars(dlgmlals3, w_buf);
    return dlgmlals3;
}

 

 

> Android.mk 정의하여 빌드 스크립트를 작성

Jni cpp 코드를 빌드할때 사용합니다.

빌드가 완료되면 jniCalculator.so 파일이 생성이 됩니다. 

// Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := jniCalculator

LOCAL_SRC_FILES := Calculator.cpp

LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)

 

> Application.mk 생성

// Application.mk
APP_ABI := armeabi-v7a

externalNativeBuild 사용하기 때문에 생략가능.

 

3. Graddle 구성

build.gradle.kts 파일 내부에 넣습니다.

android {
  	....
    externalNativeBuild {
        ndkBuild {
            path("src/main/jni/Android.mk")
        }
    }
}

 

'Opengles 3.0 with Android' 카테고리의 다른 글

Chapter 2.1 Vertex Buffer Object  (0) 2025.02.21
Chapter 1.0 EGL Context ( Skip )  (0) 2025.02.21
10.2 Object Picking ( with Raytracing )  (0) 2025.01.30
10.1 Scene Graph (Transformation Graph)  (0) 2025.01.30
9_4 엠보싱 효과  (0) 2024.12.29