Android

Android OpenGL ES 3.0 Cube 직육면체 그리기(java 버전)

카피코딩 2023. 12. 5. 21:14

 

 

이전 글에서 Google에서 학습을 위해 제공하는 OpenGL ES 3.0 관련 소스를 이용하여 삼각형과 사각형을 그려보았습니다.

 

Android OpenGL ES 3.0 삼각형 그리기(java 버전)

 


Android OpenGL ES 3.0 사각형 그리기(java 버전)

 

 

오늘은 직육면체를 그려보려고 하는데 소스는 구글 개발자 사이트에서 찾을 수 없어 인터넷을 찾아보다가 https://github.com/JimSeker/opengl 에서 적당한 소스를 찾아 소개를 드립니다.

프로그램 구성이 구글에서 설명하는 방식과 거의 동일한 구조로 되어있어서 이해하기가 쉬울것 같아 선택했습니다.

 

최종 구현 모습은 여러 곳에서 한번쯤 본듯한 형태입니다.

 

 

 

 

사각형을 그릴때 설명한것 처럼 삼각형을 2개 연결해서 사각형을 그렸습니다.  이번에도 삼각형을 그려서 사각형을 만들고 이 사각형들을 모아서 6면체를 만들게 됩니다.

사용되는 java 프로그램 소스는 처음에 삼각형 그리기 편에서 설명했던 내용이 변함 없이 적용 됩니다.  기본이되는 2개의 클래스가 있는데 이 클래스에 대한 자세한 설명은 삼각형 그리기를 참고하면 되고 여기서는 간략하게 어떤 내용이 있었는지 제목 정도만 알아보겠습니다.

 

GLSurfaceView 클래스 : 객체를 그리고 조작할 수 있는 View 이며 터치 리스너를 구현할 수 있습니다.

 

GLSurfaceView.Renderer 클래스 : 3개의 기본 메서드를 구현해야합니다.

  - onSurfaceCreated() : 최초 한번 호출되며 초기화가 필요한 내용 기술.

  - onDrawFrame() : GLSurfaceView를 다시 그릴때마다 호출되며 Object를 움직이거나 변형 가능.

  - onSurfaceChanged() : GLSurfaceView의 크기, 방향 변경시 호출된다.

 

 

다른 도형과 다르게 Cube 그리기에서 추가되는 부분이 있는데 터치 관련 기능입니다.  GLSurfaceVIew 클래스 설명에 터치 이벤트를 구현한다고 되어있는것 처럼 GLSurfaceView 클래스에 onTouchEvent()가 추가 됩니다.  구현할 때

 

private static final float TOUCH_SCALE_FACTOR = 0.0015f;

 

이 값을 변경하면 화면을 터치하고 Object를 이동 시킬때 한번에 이동하는 거리를 조절할 수 있습니다.

 

Renderer 프로그램에는 onDrawFrame()에서 물체의 변형이나 움직임을 구현할 수 있다고 하였는데 여기서는 육면체를 회전시키는 작업이 추가됩니다.  코드에서

 

mAngle+=4;

 

이 각도값을 변경하면 회전하는 속도를 조절할 수 있습니다.

 

myColor.java 라는 파일이 하나 추가되었는데 삼각형, 사각형은 2차원 이미지라 색상 값을 그냥 코드에 추가를 하였는데 정육면체는 6개의 색을 표현하기 위해 별도의 파일로 만들어 놓았습니다. 귀찮으면 그냥 파일을 만들지 말고 구현하거나 단색으로 테스트해도 됩니다.

설명을 이정도로 하고 실행 했을 때의 모습을 보겠습니다.

 

 

 

 

아래는 전체 코드입니다.  수정하지 않은 파일을 첨부하지 않았습니다.

위에서 소개한 페이지로 가서 소스를 다운받아 테스트 하셔도 됩니다.

 

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.OpenGLTest"

        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>

 

 

 

 

myGlSurfaceView.java

 

package com.example.opengltest;



import android.content.Context;

import android.opengl.GLSurfaceView;

import android.view.MotionEvent;



public class myGlSurfaceView extends GLSurfaceView {



    myRenderer myRender;



    public myGlSurfaceView(Context context) {

        super(context);



        // Create an OpenGL ES 3.0 context.

        setEGLContextClientVersion(3);



        super.setEGLConfigChooser(8, 8, 8, 8, 16, 0);



        // Set the Renderer for drawing on the GLSurfaceView

        myRender = new myRenderer(context);

        setRenderer(myRender);



        // Render the view only when there is a change in the drawing data

        setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);

    }



    //private final float TOUCH_SCALE_FACTOR = 180.0f / 320;

    private static final float TOUCH_SCALE_FACTOR = 0.0015f;

    private float mPreviousX;

    private float mPreviousY;



    @Override

    public boolean onTouchEvent(MotionEvent e) {

//        return super.onTouchEvent(event);



        float x = e.getX();

        float y = e.getY();



        switch (e.getAction()) {

            case MotionEvent.ACTION_MOVE:



                float dx = x - mPreviousX;

                //subtract, so the cube moves the same direction as your finger.

                //with plus it moves the opposite direction.

                myRender.setX(myRender.getX() - (dx * TOUCH_SCALE_FACTOR));



                float dy = y - mPreviousY;

                myRender.setY(myRender.getY() - (dy * TOUCH_SCALE_FACTOR));

        }



        mPreviousX = x;

        mPreviousY = y;

        return true;

    }

}

 

 

 

 

myRenderer.java

 

package com.example.opengltest;



import android.content.Context;

import android.opengl.GLES30;

import android.opengl.GLSurfaceView;

import android.opengl.Matrix;

import android.util.Log;



import javax.microedition.khronos.egl.EGLConfig;

import javax.microedition.khronos.opengles.GL10;



public class myRenderer implements GLSurfaceView.Renderer {



    private int mWidth;

    private int mHeight;

    private static String TAG = "myRenderer";

    public Cube mCube;

    private float mAngle =0;

    private float mTransY=0;

    private float mTransX=0;

    private static final float Z_NEAR = 1f;

    private static final float Z_FAR = 40f;



    // mMVPMatrix is an abbreviation for "Model View Projection Matrix"

    private final float[] mMVPMatrix = new float[16];

    private final float[] mProjectionMatrix = new float[16];

    private final float[] mViewMatrix = new float[16];

    private final float[] mRotationMatrix = new float[16];





    public myRenderer(Context context) {

        //cube can not be instianated here, because of "no egl context"  no clue.

        //do it in onSurfaceCreate and it is fine.  odd, but workable solution.

    }

    public static int LoadShader(int type, String shaderSrc) {

        int shader;

        int[] compiled = new int[1];



        // Create the shader object

        shader = GLES30.glCreateShader(type);



        if (shader == 0) {

            return 0;

        }



        // Load the shader source

        GLES30.glShaderSource(shader, shaderSrc);



        // Compile the shader

        GLES30.glCompileShader(shader);



        // Check the compile status

        GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, compiled, 0);



        if (compiled[0] == 0) {

            Log.e(TAG, "Erorr!!!!");

            Log.e(TAG, GLES30.glGetShaderInfoLog(shader));

            GLES30.glDeleteShader(shader);

            return 0;

        }



        return shader;

    }



    public static void checkGlError(String glOperation) {

        int error;

        while ((error = GLES30.glGetError()) != GLES30.GL_NO_ERROR) {

            Log.e(TAG, glOperation + ": glError " + error);

            throw new RuntimeException(glOperation + ": glError " + error);

        }

    }



    @Override

    public void onSurfaceCreated(GL10 gl, EGLConfig config) {



        //set the clear buffer color to light gray.

        //GLES30.glClearColor(0.9f, .9f, 0.9f, 0.9f);

        //set the clear buffer color to a dark grey.

        GLES30.glClearColor(0.1f, .1f, 0.1f, 0.9f);

        //initialize the cube code for drawing.

        mCube = new Cube();

        //if we had other objects setup them up here as well.

    }



    @Override

    public void onSurfaceChanged(GL10 gl, int width, int height) {



        mWidth = width;

        mHeight = height;

        // Set the viewport

        GLES30.glViewport(0, 0, mWidth, mHeight);

        float aspect = (float) width / height;



        // this projection matrix is applied to object coordinates

        //no idea why 53.13f, it was used in another example and it worked.

        Matrix.perspectiveM(mProjectionMatrix, 0, 53.13f, aspect, Z_NEAR, Z_FAR);

    }



    @Override

    public void onDrawFrame(GL10 gl) {



        // Clear the color buffer  set above by glClearColor.

        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT | GLES30.GL_DEPTH_BUFFER_BIT);



        //need this otherwise, it will over right stuff and the cube will look wrong!

        GLES30.glEnable(GLES30.GL_DEPTH_TEST);



        // Set the camera position (View matrix)  note Matrix is an include, not a declared method.

        Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);



        // Create a rotation and translation for the cube

        Matrix.setIdentityM(mRotationMatrix, 0);



        //move the cube up/down and left/right

        Matrix.translateM(mRotationMatrix, 0, mTransX, mTransY, 0);



        //mangle is how fast, x,y,z which directions it rotates.

        Matrix.rotateM(mRotationMatrix, 0, mAngle, 1.0f, 1.0f, 1.0f);



        // combine the model with the view matrix

        Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mRotationMatrix, 0);



        // combine the model-view with the projection matrix

        Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0);



        mCube.draw(mMVPMatrix);



        //change the angle, so the cube will spin.

        mAngle+=4;

    }



    //used the touch listener to move the cube up/down (y) and left/right (x)

    public float getY() {

        return mTransY;

    }



    public void setY(float mY) {

        mTransY = mY;

    }



    public float getX() {

        return mTransX;

    }



    public void setX(float mX) {

        mTransX = mX;

    }

}

 

 

Cube.java

 

package com.example.opengltest;



import android.opengl.GLES30;

import android.util.Log;



import java.nio.ByteBuffer;

import java.nio.ByteOrder;

import java.nio.FloatBuffer;



public class Cube {



    private int mProgramObject;

    private int mMVPMatrixHandle;

    private int mColorHandle;

    private FloatBuffer mVertices;



    //initial size of the cube.  set here, so it is easier to change later.

    float size = 0.4f;



    //this is the initial data, which will need to translated into the mVertices variable in the consturctor.

    float[] mVerticesData = new float[]{

            ////////////////////////////////////////////////////////////////////

            // FRONT

            ////////////////////////////////////////////////////////////////////

            // Triangle 1

            -size, size, size, // top-left

            -size, -size, size, // bottom-left

            size, -size, size, // bottom-right

            // Triangle 2

            size, -size, size, // bottom-right

            size, size, size, // top-right

            -size, size, size, // top-left

            ////////////////////////////////////////////////////////////////////

            // BACK

            ////////////////////////////////////////////////////////////////////

            // Triangle 1

            -size, size, -size, // top-left

            -size, -size, -size, // bottom-left

            size, -size, -size, // bottom-right

            // Triangle 2

            size, -size, -size, // bottom-right

            size, size, -size, // top-right

            -size, size, -size, // top-left



            ////////////////////////////////////////////////////////////////////

            // LEFT

            ////////////////////////////////////////////////////////////////////

            // Triangle 1

            -size, size, -size, // top-left

            -size, -size, -size, // bottom-left

            -size, -size, size, // bottom-right

            // Triangle 2

            -size, -size, size, // bottom-right

            -size, size, size, // top-right

            -size, size, -size, // top-left

            ////////////////////////////////////////////////////////////////////

            // RIGHT

            ////////////////////////////////////////////////////////////////////

            // Triangle 1

            size, size, -size, // top-left

            size, -size, -size, // bottom-left

            size, -size, size, // bottom-right

            // Triangle 2

            size, -size, size, // bottom-right

            size, size, size, // top-right

            size, size, -size, // top-left



            ////////////////////////////////////////////////////////////////////

            // TOP

            ////////////////////////////////////////////////////////////////////

            // Triangle 1

            -size, size, -size, // top-left

            -size, size, size, // bottom-left

            size, size, size, // bottom-right

            // Triangle 2

            size, size, size, // bottom-right

            size, size, -size, // top-right

            -size, size, -size, // top-left

            ////////////////////////////////////////////////////////////////////

            // BOTTOM

            ////////////////////////////////////////////////////////////////////

            // Triangle 1

            -size, -size, -size, // top-left

            -size, -size, size, // bottom-left

            size, -size, size, // bottom-right

            // Triangle 2

            size, -size, size, // bottom-right

            size, -size, -size, // top-right

            -size, -size, -size // top-left

    };



    float colorcyan[] = myColor.cyan();

    float colorblue[] = myColor.blue();

    float colorred[] = myColor.red();

    float colorgray[] = myColor.gray();

    float colorgreen[] = myColor.green();

    float coloryellow[] = myColor.yellow();



    //vertex shader code

    String vShaderStr =

            "#version 300 es           \n"

                    + "uniform mat4 uMVPMatrix;     \n"

                    + "in vec4 vPosition;           \n"

                    + "void main()                  \n"

                    + "{                            \n"

                    + "   gl_Position = uMVPMatrix * vPosition;  \n"

                    + "}                            \n";

    //fragment shader code.

    String fShaderStr =

            "#version 300 es                          \n"

                    + "precision mediump float;                   \n"

                    + "uniform vec4 vColor;                      \n"

                    + "out vec4 fragColor;                   \n"

                    + "void main()                                  \n"

                    + "{                                            \n"

                    + "  fragColor = vColor;                       \n"

                    + "}                                            \n";



    String TAG = "Cube";





    //finally some methods

    //constructor

    public Cube() {

        //first setup the mVertices correctly.

        mVertices = ByteBuffer

                .allocateDirect(mVerticesData.length * 4)

                .order(ByteOrder.nativeOrder())

                .asFloatBuffer()

                .put(mVerticesData);

        mVertices.position(0);



        //setup the shaders

        int vertexShader;

        int fragmentShader;

        int programObject;

        int[] linked = new int[1];



        // Load the vertex/fragment shaders

        vertexShader = myRenderer.LoadShader(GLES30.GL_VERTEX_SHADER, vShaderStr);

        fragmentShader = myRenderer.LoadShader(GLES30.GL_FRAGMENT_SHADER, fShaderStr);



        // Create the program object

        programObject = GLES30.glCreateProgram();



        if (programObject == 0) {

            Log.e(TAG, "So some kind of error, but what?");

            return;

        }



        GLES30.glAttachShader(programObject, vertexShader);

        GLES30.glAttachShader(programObject, fragmentShader);



        // Bind vPosition to attribute 0

        GLES30.glBindAttribLocation(programObject, 0, "vPosition");



        // Link the program

        GLES30.glLinkProgram(programObject);



        // Check the link status

        GLES30.glGetProgramiv(programObject, GLES30.GL_LINK_STATUS, linked, 0);



        if (linked[0] == 0) {

            Log.e(TAG, "Error linking program:");

            Log.e(TAG, GLES30.glGetProgramInfoLog(programObject));

            GLES30.glDeleteProgram(programObject);

            return;

        }



        // Store the program object

        mProgramObject = programObject;



        //now everything is setup and ready to draw.

    }



    public void draw(float[] mvpMatrix) {



        // Use the program object

        GLES30.glUseProgram(mProgramObject);



        // get handle to shape's transformation matrix

        mMVPMatrixHandle = GLES30.glGetUniformLocation(mProgramObject, "uMVPMatrix");

        myRenderer.checkGlError("glGetUniformLocation");



        // get handle to fragment shader's vColor member

        mColorHandle = GLES30.glGetUniformLocation(mProgramObject, "vColor");





        // Apply the projection and view transformation

        GLES30.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);

        myRenderer.checkGlError("glUniformMatrix4fv");



        int VERTEX_POS_INDX = 0;

        mVertices.position(VERTEX_POS_INDX);  //just in case.  We did it already though.



        //add all the points to the space, so they can be correct by the transformations.

        //would need to do this even if there were no transformations actually.

        GLES30.glVertexAttribPointer(VERTEX_POS_INDX, 3, GLES30.GL_FLOAT,

                false, 0, mVertices);

        GLES30.glEnableVertexAttribArray(VERTEX_POS_INDX);



        //Now we are ready to draw the cube finally.

        int startPos =0;

        int verticesPerface = 6;



        //draw front face

        GLES30.glUniform4fv(mColorHandle, 1, colorblue, 0);

        GLES30.glDrawArrays(GLES30.GL_TRIANGLES,startPos,verticesPerface);

        startPos += verticesPerface;



        //draw back face

        GLES30.glUniform4fv(mColorHandle, 1, colorcyan, 0);

        GLES30.glDrawArrays(GLES30.GL_TRIANGLES, startPos, verticesPerface);

        startPos += verticesPerface;



        //draw left face

        GLES30.glUniform4fv(mColorHandle, 1, colorred, 0);

        GLES30.glDrawArrays(GLES30.GL_TRIANGLES,startPos,verticesPerface);

        startPos += verticesPerface;



        //draw right face

        GLES30.glUniform4fv(mColorHandle, 1, colorgray, 0);

        GLES30.glDrawArrays(GLES30.GL_TRIANGLES,startPos,verticesPerface);

        startPos += verticesPerface;



        //draw top face

        GLES30.glUniform4fv(mColorHandle, 1, colorgreen, 0);

        GLES30.glDrawArrays(GLES30.GL_TRIANGLES,startPos,verticesPerface);

        startPos += verticesPerface;



        //draw bottom face

        GLES30.glUniform4fv(mColorHandle, 1, coloryellow, 0);

        GLES30.glDrawArrays(GLES30.GL_TRIANGLES,startPos,verticesPerface);

        //last face, so no need to increment.



    }

}

 

 

 

 

myColor.java

 

package com.example.opengltest;



import android.graphics.Color;



public class myColor {



    static float[] red() {

        return new float[]{

                Color.red(Color.RED) / 255f,

                Color.green(Color.RED) / 255f,

                Color.blue(Color.RED) / 255f,

                1.0f

        };

    }



    static float[] green() {

        return new float[]{

                Color.red(Color.GREEN) / 255f,

                Color.green(Color.GREEN) / 255f,

                Color.blue(Color.GREEN) / 255f,

                1.0f

        };

    }



    static float[] blue() {

        return new float[]{

                Color.red(Color.BLUE) / 255f,

                Color.green(Color.BLUE) / 255f,

                Color.blue(Color.BLUE) / 255f,

                1.0f

        };

    }



    static float[] yellow() {

        return new float[]{

                Color.red(Color.YELLOW) / 255f,

                Color.green(Color.YELLOW) / 255f,

                Color.blue(Color.YELLOW) / 255f,

                1.0f

        };

    }



    static float[] cyan() {

        return new float[]{

                Color.red(Color.CYAN) / 255f,

                Color.green(Color.CYAN) / 255f,

                Color.blue(Color.CYAN) / 255f,

                1.0f

        };

    }



    static float[] gray() {

        return new float[]{

                Color.red(Color.GRAY) / 255f,

                Color.green(Color.GRAY) / 255f,

                Color.blue(Color.GRAY) / 255f,

                1.0f

        };

    }

}

 

 

MainActivity.java

 

package com.example.opengltest;



import androidx.appcompat.app.AppCompatActivity;



import android.app.ActivityManager;

import android.content.Context;

import android.content.pm.ConfigurationInfo;

import android.os.Bundle;

import android.util.Log;

import android.view.View;



public class MainActivity extends AppCompatActivity {



    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

//        setContentView(R.layout.activity_main);



        if (detectOpenGLES30()) {

            //so we know it a opengl 3.0 and use our extended GLsurfaceview.

            setContentView(new myGlSurfaceView(this));

        } else {

            // This is where you could create an OpenGL ES 2.0 and/or 1.x compatible

            // renderer if you wanted to support both ES 1 and ES 2.

            Log.e("openglcube", "OpenGL ES 3.0 not supported on device.  Exiting...");

            finish();



        }

    }



    private boolean detectOpenGLES30() {

        ActivityManager am =

                (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);

        ConfigurationInfo info = am.getDeviceConfigurationInfo();

        return (info.reqGlEsVersion >= 0x30000);

    }



    @Override

    public void onWindowFocusChanged(boolean hasFocus) {

        super.onWindowFocusChanged(hasFocus);



        if (hasFocus) {

            getWindow().getDecorView().setSystemUiVisibility(

                    View.SYSTEM_UI_FLAG_LAYOUT_STABLE

                            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

                            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN

                            | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION

                            | View.SYSTEM_UI_FLAG_FULLSCREEN

                            | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);

        }

    }

}

 

 - copy coding -