본문 바로가기
DEVLOG/Android

## OpenGL을 이용한 Solar System

2019. 4. 29.
반응형

Solar System

OpenGL의 기본적인 기능들을 이용하여 태양계를 구성

  • Viewport, Projection, Viewing transform
  • Texture 2D mapping
  • Transformation matrix
  • ModelView, Projection mode

Viewport Transform

void gl.glViewport(int x, int y, int width, int height)
  • x, y : Viewport 사각형의 왼쪽 아래 코너의 좌표, default는 (0, 0)
  • width, height : Viewport 영역의 폭과 높이

Projection Transform

void GLU.gluPerspective(GL10 gl, float fovy, float aspect, float zNear, float zFar)
  • fovy : 수직 방향의 field of view (단위 : degree)
  • aspect : Clipping window의 가로:세로 비율 (가로/세로)
  • zNear : Near plane까지의 거리 (>0)
  • zFar : Far plane 까지의 거리 (>0)

Viewing Transform

void GLU.glLookat(GL10 gl, float eyeX, float eyeY, float eyeZ, 
		float centerX, float centerY, float centerZ, 
		float upX, float upY, float upZ)
  • eyeX, eyeY, eyeZ : 카메라(눈)의 위치
  • centerX, centerY, centerZ : 카메라가 바라보는 점의 위치
  • upX, upY, upZ : 카메라 위쪽의 방향 (Up vector)

Transformation Matrix

  • Modelview matrix M
    • gl.glMatrixMode(GL10.GL_MODELVIEW); 로 활성화
    • 좌표계에서 물체의 변환, 시점의 이동 등 반영
  • Projection matrix P
    • gl.glMatrixMode(GL10.GL_PROJECTION); 로 활성화
    • 카메라의 projection 특성 반영
    • 예) 평형 투사, 원근 투사 등
  • gl.glLoadIdentity()
    • 현재 활성화된 행렬을 단위 행렬로 초기화
  • 변환에 관련된 명령 수행 시
    • 현재 활성화된 행렬(M 또는 P)에 명령이 의미하는 변환이 곱해짐
    • 현재 활성화된 행렬이 M이고 gl.glTranslate(행렬 T 생성) 수행 시 M <- MT로 바뀜 (post-multiplication)
    • 따라서 실제 적용되는 변환의 역순으로 GL 명령들을 수행
    • 3차원 점 X에 대해서는 최종적으로 PMX와 같이 변환되어 viewport 상의 최종적인 좌표 생성
  • void gl.glLoadMatrixf(float[] m, int offset)
    • 배열 m을 이용한 변환 행렬 설정
    • 변환 matrix를 배열 m의 matrix로 초기화
  • void gl.glMultMatrixf(float[] m, int offset)
    • 배열 m을 이용한 변환 행렬 설정
    • 변환 matrix에 배열 m에 따른 matrix의 post-multiplication

gl.glMatrixMode(gl.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glMultMatrix(T, 0);
gl.glMultMatrix(R, 0);

 

gl.glMatrixMode(gl.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glMultMatrix(R, 0);
gl.glMultMatrix(T, 0);

 

  • void gl.glTranslatef(float x, float y, float z);
    • 평행이동에 해당하는 matrix를 현재 matrix에 곱함
    • x, y, z : 평행 이동량
  • void gl.glRotatef(float angle, float x, float y, float z);
    • 회전이동에 해당하는 matrix를 현재 matrix에 곱함
    • angle : 회전각
    • x, y, z : 회전 중심축
  • void gl.glScalef(float x, float y, float z);
    • 크기변화에 해당하는 matrix를 현재 matrix에 곱함
    • x, y, z : 각 축 방향 Scaling 배수

 

Manifest

항상 landscape 모드로 유지하도록 설정

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.solarsystem_obj_demo">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity"
            android:screenOrientation="landscape">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

그림 파일을 <Project path>/app/src/main/res/drawable에 저장합니다.

Layout attribute로 android:background="@drawable/background"

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:background="@drawable/background">

    <Button
        android:id="@+id/btnTextureOnOff"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:onClick="texture_on_off"
        android:text="Texture"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btnRotationOnOff" />

    <Button
        android:id="@+id/btnRotationOnOff"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:onClick="rotation_on_off"
        android:text="Rotation"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

 

Main Activity

Touch event를 위한 변수들 및 OpenGL을 사용하기 위해 GLSurfaceView, Renderer(SolarSystemRenderer) 선언, 상단바 제거 및 Renderer 지정

public class MainActivity extends AppCompatActivity {
    private final double TOUCH_SCALE_FACTOR = 0.1;
    private double preAzim;
    private double preElev;
    private GLSurfaceView surfaceView;
    private SolarSystemRenderer solarSystem = new SolarSystemRenderer(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);

        View decorView = getWindow().getDecorView();

        int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
        decorView.setSystemUiVisibility(uiOptions);

        ActionBar actionBar = getSupportActionBar();
        actionBar.hide();

        setContentView(R.layout.activity_main);

        surfaceView = new GLSurfaceView(this);
        surfaceView.setEGLConfigChooser( 8, 8, 8, 8, 16, 0 );
        surfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
        surfaceView.setRenderer(solarSystem);
        surfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
        surfaceView.setZOrderOnTop(true);

        addContentView(surfaceView, new RelativeLayout.LayoutParams(
                RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT));
    }

Touch를 하여 화면 조작을 위한 event 지정

x, y 방향으로의 변화량을 통해 태양계를 바라보는 카메라의 위도, 경도를 변경

0~360˚로 범위 제한

텍스쳐 및 행성 자전, 공전 토글 버튼 함수 정의

@Override
    public boolean onTouchEvent(MotionEvent e) {
        double azim = e.getX();
        double elev = e.getY();

        switch (e.getAction()) {
            case MotionEvent.ACTION_MOVE:
                double dAzim = azim - preAzim;
                double dElev = elev - preElev;

                solarSystem.azim += dAzim * TOUCH_SCALE_FACTOR;
                solarSystem.elev += dElev * TOUCH_SCALE_FACTOR;

                if(solarSystem.azim > 360.0f) {
                    solarSystem.azim -= 360.0f;
                }

                if(solarSystem.azim < 0.0f) {
                    solarSystem.azim += 360.0f;
                }

                if(solarSystem.elev > 360.0f) {
                    solarSystem.elev -= 360.0f;
                }

                if(solarSystem.elev < 0.0f) {
                    solarSystem.elev += 360.0f;
                }

                surfaceView.requestRender();
        }

        preAzim = azim;
        preElev = elev;
        return true;
    }

    public void texture_on_off(View view){
        solarSystem.texture_on_off = !solarSystem.texture_on_off;
    }

    public void rotation_on_off(View view){
        solarSystem.rot_flag = !solarSystem.rot_flag;
    }
}

 

SolarSystemRenderer.java

태양계를 그려주기 위해 GLSurfaceView.Renderer를 Implementing하는 class 생성

행성들, 조명, 재질, 회전 및 카메라를 위한 전역 변수들 선언

public class SolarSystemRenderer implements GLSurfaceView.Renderer {
    private Context context;

    /* For obj(planet) & texture */
    private ObjStructure[] planet = new ObjStructure[10];

    public int[] texture_id = new int[]{    // 태양, 수성, 금성, 지구, 달, 화성, 목성, 토성, 천왕성, 해왕성
            R.drawable.sun,R.drawable.mercury,R.drawable.venus,
            R.drawable.earth,R.drawable.moon,R.drawable.mars,
            R.drawable.jupiter, R.drawable.saturn, R.drawable.uranus,
            R.drawable.neptune};

    public float scaler = .5f;  // 태양 크기 결정
    public float[] scalefactor = new float[]{   // 태양으로부터 상대적 크기 결정
            scaler, scaler*0.1f, scaler*0.2f,   // 태양, 수성, 금성
            scaler*0.25f, scaler*0.08f, scaler*0.18f,    // 지구, 달, 화성
            scaler*0.5f, scaler*0.4f,scaler*0.3f,scaler*0.3f};  // 목성, 토성, 천왕성, 해왕성

    /* For rotation */
    public boolean rot_flag = true;
    private float rot_sun = 360.0f;
    private float angle_mercury = 1.0f;
    private float angle_venus = 2.6f;
    private float angle_earth = 2.0f;
    private float angle_moon = 1.4f;
    private float angle_mars = 1.3f;
    private float angle_jupiter = 2.5f;
    private float angle_saturn = 2.2f;
    private float angle_uranus = 1.5f;
    private float angle_neptune = 2.7f;


    /* For camera setting */
    private double distance;
    public volatile double elev;
    public volatile double azim;

    private float[] cam_eye = new float[3];
    private float[] cam_center = new float[3];
    private float[] cam_up = new float[3];
    private float[] cam_vpn = new float[3];
    private float[] cam_x_axis = new float[3];

    private float[] uv_py = new float[3];
    private float[] uv_ny = new float[3];

    /* For texture on, off */
    public boolean texture_on_off = false;

카메라의 up vector를 계산해주기 위해 vector의 외적(calcCross), 정규화(vNorm), up vector를 계산(calcUpVector)해주는 함수 선언

public SolarSystemRenderer(Context context) {
        this.context = context;
    }

    // vector의 외적
    private void calcCross(float[] vector1, float[] vector2, float[] cp_vector) {
        cp_vector[0] = vector1[1] * vector2[2] - vector1[2] * vector2[1];
        cp_vector[1] = vector1[2] * vector2[0] - vector1[0] * vector2[2];
        cp_vector[2] = vector1[0] * vector2[1] - vector1[1] * vector2[0];
    }

    // 정규화
    private void vNorm(float[] vector) {
        float scale = (float) Math.sqrt(Math.pow((double) vector[0], 2) + Math.pow((double) vector[1], 2) + Math.pow((double) vector[2], 2));

        vector[0] = vector[0] / scale;
        vector[1] = vector[1] / scale;
        vector[2] = vector[2] / scale;
    }

    // up vector 계산
    private void calcUpVector() {
        double r_elev = elev * Math.PI / 180.0;
        double r_azim = azim * Math.PI / 180.0;

        cam_eye[0] = (float) distance * (float) Math.sin(r_elev) * (float) Math.sin(r_azim);
        cam_eye[1] = (float) distance * (float) Math.cos(r_elev);
        cam_eye[2] = (float) distance * (float) Math.sin(r_elev) * (float) Math.cos(r_azim);

        cam_vpn[0] = cam_eye[0] - cam_center[0];
        cam_vpn[1] = cam_eye[1] - cam_center[1];
        cam_vpn[2] = cam_eye[2] - cam_center[2];
        vNorm(cam_vpn);

        if (elev >= 0 && elev < 180) {
            calcCross(uv_py, cam_vpn, cam_x_axis);
        }
        else {
            calcCross(uv_ny, cam_vpn, cam_x_axis);

        }
        calcCross(cam_vpn, cam_x_axis, cam_up);
        vNorm(cam_up);
    }

Camera Up Vector 계산

World 좌표계와 카메라 좌표계의 관계를 이용하여 카메라의 up vector(y_c 방향) 계산

다음 순서로 계산

  1. z_c (View-plane Normal) X y축 unit vector = x_c
  2. x_c X z_c = y_c
  3. y_c의 unit vector = up vector

GLRenderer를 통해 surface가 생성될 때(onSurfaceCreated) 변수 초기화

// GLRenderer를 통해 surface가 생성될 때(onSurfaceCreated) 변수 초기화
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        gl.glHint(gl.GL_PERSPECTIVE_CORRECTION_HINT, gl.GL_FASTEST);
        gl.glEnable(gl.GL_DEPTH_TEST);
        gl.glEnable(gl.GL_CULL_FACE);
        gl.glCullFace(gl.GL_BACK);

        distance = 10.0;
        elev = 90.0;
        azim = 0.0;

        uv_py[0] = 0.0f;
        uv_py[1] = 1.0f;
        uv_py[2] = 0.0f;

        uv_ny[0] = 0.0f;
        uv_ny[1] = -1.0f;
        uv_ny[2] = 0.0f;

        cam_center[0] = 0.0f;
        cam_center[1] = 0.0f;
        cam_center[2] = 0.0f;

        calcUpVector();

        for(int i=0; i<10;i ++){
            ObjParser objParser = new ObjParser(context); // obj 파일 Parser 생성
            try {
                objParser.parse(R.raw.planet); // obj 파일 parsing

            } catch (IOException e) {

            }
            int group = objParser.getObjectIds().size(); // 몇 개의 obj 파일이 있는지 확인
            int[] texture = new int[group];
            texture[0] = texture_id[i]; // texture 파일 설정

            planet[i] = new ObjStructure(objParser, gl, this.context, texture); // objstructure 생성
        }
        gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL10.GL_REPLACE);
        gl.glEnable(GL10.GL_DEPTH_TEST);
    }

 

화면의 해상도가 변경될 때(onSurfaceChanged) perspective와 viewport 지정

// 화면의 해상도가 변경될 때(onSurfaceChanged) perspective와 viewport 지정
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        float zNear = 0.1f;
        float zFar = 1000f;
        float fovy = 45.0f;
        float aspect = (float) width / (float) height;

        /**
         * Projection matrix P
         * 카메라의 projection 특성 반영
         * 예) 평형 투사, 원근 투사 등
         */
        gl.glMatrixMode(GL10.GL_PROJECTION);
        gl.glLoadIdentity(); // 현재 활성화된 행렬을 단위 행렬로 초기화

        /**
         * fovy: 수직 방향의 field of view (단위: degree)
         * aspect: Clipping window의 가로:세로 비율(가로/세로)
         * zNear: Near plane 까지의 거리 (>0)
         * zFar: Far plane 까지의 거리 (>0)
         */
        GLU.gluPerspective(gl, fovy, aspect, zNear, zFar);
        gl.glViewport(0, 0, width, height);
        // x,y: Viewport 사각형의 왼쪽 아래 코너의 좌표
        // width, height: Viewport 영역의 폭과 높이
    }

직접 행성들을 그릴 때(onDrawFrame) color 및 depth buffer 초기화

카메라 위치 조정(gluLookAt)

gl.glPushMatrix(), glPopMatrix()를 이용하여 각 행성 별 공전, 자전이 개별적으로 가능하도록 설정

    // 직접 행성들을 그릴 때(onDrawFrame) color 및 depth buffer 초기화
    public void onDrawFrame(GL10 gl) {
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
        gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        gl.glClearDepthf(1.0f);

        /**
         * ModelView matirx M
         * 좌표계에서 물체의 변환, 시점의 이동 등 반영
         */
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity(); // 현재 활성화된 행렬을 단위 행렬로 초기화

        calcUpVector();

        /**
         * gluLookAt 카메라 위치 지정
         * eyeX, eyeY, eyeZ: 카메라(눈)의 위치
         * centerX, centerY, centerZ: 카메라가 바라보는 점의 위치
         * upX, upY, upZ: 카메라 위쪽의 방향(Up vector)
         */
        GLU.gluLookAt(gl, cam_eye[0], cam_eye[1], cam_eye[2], cam_center[0], cam_center[1], cam_center[2], cam_up[0], cam_up[1], cam_up[2]);

        if(texture_on_off){
            gl.glEnable(GL10.GL_TEXTURE_2D);
        }else{
            gl.glDisable(GL10.GL_TEXTURE_2D);
        }

        /**
         * gl.glPushMatrix(), gl.glPopMatrix()를 이용하여
         * 각 행성별 공전, 자전이 개별적으로 가능하도록 설정
         */
        gl.glPushMatrix();
            gl.glRotatef(rot_sun, 0.0f, 1.0f, 0.0f); // 태양의 자전
            // draw Sun
            gl.glColor4f(1.0f,0.0f,0.0f,1.0f);
            planet[0].setScale(scalefactor[0]);
            planet[0].draw(gl);
        gl.glPopMatrix();
        /**
         * gl.glRotatef(float angle, float x, float y, float z)
         * 회전이동에 해당하는 matrix를 현재 matrix에 곱함
         * angle: 회전각
         * x, y, z: 회전 중심축
         */
        /**
         * gl.glTranslatef(float x, float y, float z)
         * 평행이동에 해당하는 matrix를 현재 matrix에 곱함
         * x, y, z: 평행 이동량
         */
        gl.glPushMatrix();
            gl.glRotatef(angle_mercury, 0.0f, 1.0f, 0.0f); // 수성의 자전
            gl.glTranslatef(1.3f, 0.0f, 0.0f);

            // draw Mercury
            gl.glColor4f(0.0f, 1.0f, 1.0f, 1.0f);
            planet[1].setScale(scalefactor[1]);
            planet[1].draw(gl);
        gl.glPopMatrix();

        gl.glPushMatrix();
            gl.glRotatef(angle_venus, 0.0f, 1.0f, 0.0f); // 금성의 자전
            gl.glTranslatef(1.6f, 0.0f, 0.0f);

            // draw Venus
            gl.glColor4f(1.0f, 1.0f, 0.0f, 1.0f);
            planet[2].setScale(scalefactor[2]);
            planet[2].draw(gl);
        gl.glPopMatrix();

        gl.glPushMatrix(); // 태양 기준의 좌표계를 스택에 푸시하여 저장
            gl.glPushMatrix(); // 지구 기준의 좌표계를 스택에 푸시하여 저장
                gl.glRotatef(angle_earth, 0.0f, 1.0f, 0.0f); // 지구의 자전
                gl.glTranslatef(2.0f, 0.0f, 0.0f);

                // draw Earth
                gl.glColor4f(0.0f,0.0f,1.0f,1.0f);
                planet[3].setScale(scalefactor[3]);
                planet[3].draw(gl);
                gl.glPushMatrix();
                gl.glRotatef(angle_moon, 0.0f, 1.0f, 0.0f); // 달의 자전
                gl.glTranslatef(0.5f, 0.0f, 0.0f);

                // draw Moon
                gl.glColor4f(0.0f, 0.0f, 0.0f, 1.0f);
                planet[4].setScale(scalefactor[4]);
                planet[4].draw(gl);
            gl.glPopMatrix();
        gl.glPopMatrix();

        gl.glPushMatrix();
            gl.glRotatef(angle_mars, 0.0f, 1.0f, 0.0f); // 화성의 자전
            gl.glTranslatef(3.0f, 0.0f, 0.0f);

            // draw Moon
            gl.glColor4f(0.0f, 0.0f, 0.0f, 1.0f);
            planet[5].setScale(scalefactor[5]);
            planet[5].draw(gl);
        gl.glPopMatrix();

        gl.glPushMatrix();
            gl.glRotatef(angle_jupiter, 0.0f, 1.0f, 0.0f); // 목성의 자전
            gl.glTranslatef(4.0f, 0.0f, 0.0f);

            // draw Moon
            gl.glColor4f(0.0f, 0.0f, 0.0f, 1.0f);
            planet[6].setScale(scalefactor[6]);
            planet[6].draw(gl);
        gl.glPopMatrix();

        gl.glPushMatrix();
            gl.glRotatef(angle_saturn, 0.0f, 1.0f, 0.0f); // 토성의 자전
            gl.glTranslatef(5.0f, 0.0f, 0.0f);

            // draw Moon
            gl.glColor4f(0.0f, 0.0f, 0.0f, 1.0f);
            planet[7].setScale(scalefactor[7]);
            planet[7].draw(gl);
        gl.glPopMatrix();

        gl.glPushMatrix();
            gl.glRotatef(angle_uranus, 0.0f, 1.0f, 0.0f); // 천왕성의 자전
            gl.glTranslatef(6.0f, 0.0f, 0.0f);

            // draw Moon
            gl.glColor4f(0.0f, 0.0f, 0.0f, 1.0f);
            planet[8].setScale(scalefactor[8]);
            planet[8].draw(gl);
        gl.glPopMatrix();

        gl.glPushMatrix();
            gl.glRotatef(angle_neptune, 0.0f, 1.0f, 0.0f); // 해왕성의 자전
            gl.glTranslatef(7.0f, 0.0f, 0.0f);

            // draw Moon
            gl.glColor4f(0.0f, 0.0f, 0.0f, 1.0f);
            planet[9].setScale(scalefactor[9]);
            planet[9].draw(gl);
        gl.glPopMatrix();

        float orbital_mercury = 1.0f;
        float orbital_venus = 2.6f;
        float orbital_moon = 1.4f;
        float orbital_earth = 2.0f;
        float orbital_mars = 1.3f;
        float orbital_juptier = 2.5f;
        float orbital_saturn = 2.2f;
        float orbital_uranus = 1.5f;
        float orbital_neptune = 2.7f;

        if(rot_flag) {
            rot_sun -= 0.2f;
            angle_mercury += orbital_mercury;
            angle_venus += orbital_venus;
            angle_earth += orbital_earth;
            angle_moon += orbital_moon;
            angle_mars += orbital_mars;
            angle_jupiter += orbital_juptier;
            angle_saturn += orbital_saturn;
            angle_uranus += orbital_uranus;
            angle_neptune += orbital_neptune;

            if (angle_mercury >= 360.0f) {
                angle_mercury -= 360.0f;
            }

            if (angle_venus >= 360.0f) {
                angle_venus -= 360.0f;
            }

            if (angle_earth >= 360.0f) {
                angle_earth -= 360.0f;
            }

            if (angle_moon >= 360.0f) {
                angle_moon -= 360.0f;
            }

            if (angle_mars >= 360.0f) {
                angle_mars -= 360.0f;
            }

            if (angle_jupiter >= 360.0f) {
                angle_jupiter -= 360.0f;
            }

            if (angle_saturn >= 360.0f) {
                angle_saturn -= 360.0f;
            }

            if (angle_uranus >= 360.0f) {
                angle_uranus -= 360.0f;
            }

            if (angle_neptune >= 360.0f) {
                angle_neptune -= 360.0f;
            }
        }

        gl.glFlush();
    }
}

 

반응형

'DEVLOG > Android' 카테고리의 다른 글

#2 - 안드로이드 스튜디오(Android Studio) 설치하기  (0) 2019.04.29
#1 - 안드로이드 소개  (0) 2019.04.29

댓글