11.OpenGL ES

11-1.축소된 서브셋

3D 그래픽 라이브러리인 OpenGL은 태생적으로 무거울 수밖에 없다. 연산량이 많으므로 CPU는 가급적 빨라야 하며 대용량의 데이터를 다루고 다량의 버퍼를 사용하므로 메모리도 많이 소모한다. 고성능 PC 환경은 이런 요구 사항을 모두 충족시키므로 큰 무리없이 잘 실행된다. 그러나 자원의 제약이 훨씬 더 심한 모바일 장비들은 입체 그래픽을 출력하기에는 양적으로나 질적으로나 부족한 부분이 많다.

CPU는 느리고 메모리는 제한되어 있다. 게다가 항상 배터리에 의존해서 동작하므로 부족한 자원조차도 마음껏 활용하기 어렵다. 더 치명적인 것은 그래픽 하드웨어의 지원이 아예 없거나 미약해서 모든 것을 소프트웨어로 처리해야 한다는 점이다. 최악의 경우는 부동 소수점 연산조차도 소프트웨어로 수행해야 한다.

이런 제약이 심한 임베디드 환경을 위해 더 작고 가벼운 별도의 표준을 재정했는데 이것이 바로 OpenGL ES이다. ES는 Embeded System의 약자이다. ES 표준은 크로노스 그룹에서 관리한다. ES는 상위의 표준인 OpenGL에서 잘 사용되지 않거나 대체적인 방법이 있는 것을 제외하고 축소하여 만든 것이다. 즉, OpenGL의 서브셋이다. OpenGL과 ES의 관계는 다음과 같다.

 

OpenGL1.3 -> ES 1.0

OpenGL1.5 -> ES 1.1

OpenGL2.0 -> ES 2.0(2007년)

 

최신 버전인 ES 2.0은 하위 버전인 1.x와 호환되지 않는다. 1.x 버전은 고정된 파이프라인을 사용하는 데 비해 ES 2.0은 프로그래밍 가능한 쉐이더를 사용한다. 3D 그래픽을 위한 라이브러리라는 면에서만 같은 목적을 가지고 있을 뿐이며 아예 다른 표준이라고 봐도 될 정도다. 떠오르는 신기술인 HTML5의 WebGL은 ES 2.0을 기준으로 파생된 표준이다.

ES는 임베디트 환경을 위해 경량화된 것이어서 주로 OpenGL의 기능에서 일부를 제외하여 작성된 표준이다. ES 스펙 문서는 각 기능에 대한 명세를 밝히는 것보다 OpenGL에서 어떤 기능이 제외되었는지를 설명한다. 이 말은 ES의 주요 이론들이 OpenGL에서 온 것이며 ES를 배우기 위해서는 OpenGL을 먼저 배워야 한다는 뜻이다. OpenGL에 비해 ES는 다음 기능들이 제거 또는 축소되었다.

 

지원 타입의 수를 줄였다. 대표적으로 GLdouble 타입을 지원하지 않는데 사실 OpenGL에서도 이 타입은 잘 사용되지 않았다. 라이트 버전에는 GLfloat도 없고 고정 소수점 타입인 GLfixed 타입을 대신 사용한다. 임베디드 시스템에는 부동소수점 연산을 하드웨어가 수행하지 못하므로 소프트웨어로 느리게 수행해야 하기 때문이다. GLbyte, GLubyte, GLshort 타입도 제거되었다.

즉시 모드를 지원하지 않는다. glBegin~glEnd 블록안에서 그리기 함수를 호출할 수 없고 정점 배열만 사용할 수 있다. 함수 호출 오버헤드를 줄이기 위해서이다. 여러 정보를 한 배열에 저장하는 인터리브 배열도 지원하지 않는다.

사각형을 그리는 glRect 함수가 제외되었다. 그래서 모든 물체를 삼각형으로만 구성해야 한다. GL_QUADS, GL_POLYGON 모델도 제외되었다. 사실 이 기능들은 OpenGL에서도 잘 사용되지 않는 것이다.

색상 인덱스 모드는 지원하지 않는다. 조명이나 텍스처 맵핑 등에 제약이 많다. OpenGL에서도 팔레트 모드는 원래 제약이 많았다.

그외 폴리곤 모드, 피드백, 선택 기능, 누적 버퍼, 출력 목록, 속성 저장 기능을 지원하지 않는다.

텍스처 맵핑은 2D만 지원된다.

조명은 앞뒷면이 반드시 동일해야 한다. 재질 모드는 GL_AMBIENT_AND_DIFFUSE만 지원된다.

 

PC 환경에서 ES를 실습하라면 별도의 에뮬레이터가 필요하다. 그보다 더 확실한 방법은 ES를 직접 지원하는 환경에서 실습하는 것이다. 여기서는 안드로이드 환경에서 실습하기로 한다. 아이폰도 ES를 잘 지원하기는 하지만 실습 장비가 대중적이지 않아 구성하는데 비용이 많이 드는 문제가 있다. 무엇보다 본인 스스로 아이폰을 잘 몰라서 다루기 어렵다.

윈폰은 OpenGL을 지원하지도 않으며 자신의 기술인 XNA만 지원한다. 모두가 표준을 준수하고 잘 따르는데 혼자만 독불 장군이다. 이런 저런 이유로 ES 환경을 위한 최적의 실습 환경은 안드로이드이다. 굳이 장비가 없어도 에뮬레이터로 실습을 할 수 있으며 대부분의 운영체제에 개발툴을 설치할 수 있다. 이 강좌에서는 안드로이드 개발 환경에 대해서는 기본은 안다고 가정한다.

11-2.안드로이드의 그래픽 지원

안드로이드 그리기 시스템의 핵심은 뷰이다. 액티비티는 뷰를 담는 껍데기게 불과하고 입력과 출력을 담당하는 것은 뷰이다. 기본 입출력 시스템인 뷰와 ES를 연결하는 고리는 GLSurfaceView 클래스이다. GLSurfaceView는 3D 그래픽 표면을 제공하며 안드로이드의 뷰 시스템과 OpenGL을 중계한다.

OpenGL 그리기를 수행하려면 GLSurfaceView 객체를 준비해야 한다. 대부분의 기능을 메서드로 조정할 수 있으므로 굳이 상속을 받을 필요는 없다. 그냥 GLSurfaceView 타입의 객체를 액티비티의 멤버로 선언한 후 사용하면 된다. 단, 입력 메서드를 재정의할 때에만 상속을 받으면 된다.

GLSurfaceView는 중계만 담당할 뿐이며 실제로 그리기를 수행하는 것은 렌더러이다. 랜더러는 분리된 스레드에서 그리기만 수행한다. 액티비티는 onPause, onResume시에 GLSurfaceView뷰의 동일한 메서들르 반드시 호출하여 스레드를 중지 및 재개할 수 있도록 해야 한다. 랜더러는 3차원 장면을 그려낸다. 연산량이 많으므로 UI 스레드와는 다른 분리된 스레드에서 실행된다. 랜더러는 클래스가 아닌 인터페이스이며 다음 3개의 주요 메서드 제공한다. 랜더러 구현 클래스는 이 메서드들을 모두 구현해야 한다.

 

void onSurfaceCreated (GL10 gl, EGLConfig config)

그리기 표면이 생성될 때나 EGL 컨텍스트가 파되될 때 이 메서드가 호출된다. 장비가 슬립 상태로 들어갈 때 ESL 컨텍스트가 자동으로 파괴되며 관련 리소스도 모두 제거된다. 그래서 이 메서드가 호출될 때마다 관련 리소스를 다시 준비해야 한다. 모든 리소스는 자동으로 파괴되므로 일부러 파괴할 필요는 없다.

 

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

표면의 크기가 변경될 때 호출되며 생성 직후에도 호출된다. 여기서는 통상 뷰포트를 설정하고 투영 모드를 지정한다.

 

void onDrawFrame (GL10 gl)

프레임을 그릴 때마다 호출된다. 여기서 인수로 전달되는 gl의 여러 메서드를 사용하여 3차원 장면을 그린다.

 

각 메서드로 전달되는 gl 인수는 항상 GL10 타입이다. 만약 gl이 GL10보다 더 높은 버전인 경우 instanceof 연산자로 테스트해 보고 캐스팅하여 사용한다. GLSurfaceView.Renderer를 구현하는 클래스를 정의하고 GLSurfaceView의 다음 메서드로 랜더러를 지정한다.

 

void setRenderer (GLSurfaceView.Renderer renderer)

 

GLSurfaceView는 주기적으로 onDrawFrame 메서드를 호출하여 장면을 계속 갱신한다. 이런 모드를 연속 모드(RENDERMODE_CONTINUOUSLY)라고 하는데 3D 화면은 보통 계속 변하기 때문에 디폴트가 연속 모드로 되어 있다. 대표적인 3D 프로그램인 게임은 단 한순간도 쉬지 않고 계속 바뀐다. 그래서 랜더러가 별도의 스레드에서 동작하는 것이다. 만약 연속적으로 다시 그리기를 할 필요가 없다면 다음 메서드로 랜더 모드를 변경한다.

 

void setRenderMode (int renderMode)

 

RENDERMODE_WHEN_DIRTY로 변경하면 최초 한번 그리기를 수행하고 다음 그리기 요청이 있을 때까지는 화면을 갱신하지 않는다. 지도 프로그램이나 설계 도면처럼 사용자의 입력이 없으면 굳이 다시 그릴 필요가 없는 프로그램은 이 모드를 사용하는 것이 유리하다. 다시 그리기를 요청할 때는 다음 메서드를 호출한다.

 

void requestRender ()

 

이 메서드는 임의의 스레드에 호출 가능하므로 그리기 정보가 바뀐 즉시 언제든지 호출할 수 있다. 다음 예제는 2차원의 삼각형을 그린다. 안드로이드 OpenGL 프로그램의 가장 간단한 예이며 이 강좌에서 윈도우용으로 처음 만들었던 예제를 안드로이드로 포팅한 것이다.

 

Triangle

package exam.andexam_opengl;

 

import java.nio.*;

 

import javax.microedition.khronos.egl.*;

import javax.microedition.khronos.opengles.*;

 

import android.app.*;

import android.opengl.*;

import android.os.*;

 

public class Triangle extends Activity {

     GLSurfaceView mGLSurfaceView;

     public void onCreate(Bundle savedInstanceState) {

          super.onCreate(savedInstanceState);

 

          mGLSurfaceView = new GLSurfaceView(this);

          mGLSurfaceView.setRenderer(new Renderer());

          mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

          setContentView(mGLSurfaceView);

     }

 

     protected void onResume() {

          super.onResume();

          mGLSurfaceView.onResume();

     }

 

     protected void onPause() {

          super.onPause();

          mGLSurfaceView.onPause();

     }

    

     class Renderer implements GLSurfaceView.Renderer {

          public Renderer() {

          }

 

          public void onSurfaceCreated(GL10 gl, EGLConfig config) {

              gl.glClearColor(0.2f,0.2f,0.2f,1);

              gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

          }

 

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

              gl.glViewport(0, 0, width, height);

          }

 

          public void onDrawFrame(GL10 gl) {

              gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

              gl.glColor4f(1,1,0,1);

 

              float vert[] = {0, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f };

              ByteBuffer bytebuf = ByteBuffer.allocateDirect(vert.length*4);

              bytebuf.order(ByteOrder.nativeOrder());

              FloatBuffer vertbuf = bytebuf.asFloatBuffer();

              vertbuf.put(vert);

              vertbuf.position(0);

             

              gl.glVertexPointer(2, GL10.GL_FLOAT, 0, vertbuf);

              gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);

          }

     }

}

 

화면 중앙에 노란색 삼각형 하나만 그려 보았다. 장비의 화면이 세로로 길쭉하기 때문에 삼각형도 길쭉한 모양으로 그려진다.

onCreate에서 GLSurfaceView 객체를 생성하고 GLSurfaceView.Renderer 인터페이스를 구현하는 Renderer 객체를 생성하여 지정했다. 2차원의 단순한 삼각형일 뿐이고 화면이 바뀌는 것도 아니므로 랜더 모드는 비연속 모드로 지정했다. 랜더 모드를 변경하지 않으면 똑같은 화면을 계속 그리므로 CPU를 혹사하고 배터리도 금방 소진될 것이다.

랜더러가 지정된 GLSurfaceView 객체를 액티비티의 내용물로 채우면 이 표면에 랜더러가 장면을 그린다. 액티비티는 onResume, onPuase에서 GLSurfaceView의 동일 함수를 호출하여 리소스와 그리기 스레드를 관리할 수 있도록 해야 한다.

랜더러는 세 개의 메서드를 구현한다. 표면이 생성될 때 여러 가지 초기화를 할 수 있는데 배경색을 짙은 회색으로 변경하였다. OpenGL ES는 직접 그리기를 지원하지 않으므로 정점 배열을 반드시 사용해야 한다. 표면이 생성될 때 GL_VERTEX_ARRAY 기능을 켰다. 표면의 크기가 결정될 때는 뷰포트를 뷰의 크기에 맞추었다. 필요하다면 뷰의 일부에만 출력할 수도 있고 종횡비를 맞출 수도 있다.

onDrawFrame에서 배경을 먼저 지우고 정점의 색상을 노란색으로 지정했다. glClear, glColor 함수는 OpenGL과 동일하다. 다음은 삼각형을 구성하는 3개의 정점 좌표를 배열에 정의한다. 디폴트 좌표계가 -1 ~ 1 사이이므로 이 범위안에 정점을 배치했다.

glVertexPointer 등 배열을 전달받는 메서드는 배열 자체를 받지 않고 바이트 순서에 대한 정보가 설정된 Buffer 객체를 요구하므로 배열로부터 버퍼 객체를 생성해야 한다. 버퍼를 경유하여 정점 배열에 2개씩 한쌍으로 정점이 저장되어 있음을 알려 주고 glDrawArrays 메서드로 삼각형을 그렸다.

Triangle 예제는 그리기 관련 코드가 랜더러에 모두 작성되어 있으므로 한눈에 알아 보기는 쉽다. 그러나 절차적으로 나열된 코드라 관리하기에는 좋지 않고 매 장면을 그릴 때마다 배열을 다시 초기화하기 때문에 실행 속도에도 불리하다. OpenGL은 C 수준의 라이브러리이지만 안드로이드의 개발 언어인 자바는 완전한 객체 지향 언어이므로 객체 지향적으로 작성하는 것이 유리하다. 동일한 예제를 다른 형식으로 작성해 보자.

 

Triangle2

package exam.andexam_opengl;

 

import java.nio.*;

 

import javax.microedition.khronos.egl.*;

import javax.microedition.khronos.opengles.*;

 

import android.app.*;

import android.opengl.*;

import android.os.*;

 

public class Triangle2 extends Activity {

     GLSurfaceView mGLSurfaceView;

     public void onCreate(Bundle savedInstanceState) {

          super.onCreate(savedInstanceState);

 

          mGLSurfaceView = new GLSurfaceView(this);

          mGLSurfaceView.setRenderer(new Renderer());

          mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

          setContentView(mGLSurfaceView);

     }

 

     protected void onResume() {

          super.onResume();

          mGLSurfaceView.onResume();

     }

 

     protected void onPause() {

          super.onPause();

          mGLSurfaceView.onPause();

     }

    

     class Renderer implements GLSurfaceView.Renderer {

          TriangleObject triangle;

          public Renderer() {

              triangle = new TriangleObject();

          }

 

          public void onSurfaceCreated(GL10 gl, EGLConfig config) {

              gl.glClearColor(0.2f,0.2f,0.2f,1);

              gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

          }

 

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

              gl.glViewport(0, 0, width, height);

          }

 

          public void onDrawFrame(GL10 gl) {

              gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

              triangle.draw(gl);

          }

     }

    

     class TriangleObject {

          float vert[] = {0, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f };

          FloatBuffer vertbuf;

         

          public FloatBuffer ArrayToBuffer(float[] ar) {

              ByteBuffer bytebuf = ByteBuffer.allocateDirect(ar.length*4);

              bytebuf.order(ByteOrder.nativeOrder());

              FloatBuffer buf = bytebuf.asFloatBuffer();

              buf.put(ar);

              buf.position(0);

              return buf;

          }

         

          public TriangleObject() {

              vertbuf = ArrayToBuffer(vert);

          }

         

          public void draw(GL10 gl) {

              gl.glColor4f(1,1,0,1);

             

              gl.glVertexPointer(2, GL10.GL_FLOAT, 0, vertbuf);

              gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);

          }

     }

}

 

 

실행 결과는 완전히 동일하다. 삼각형과 관련된 코드가 TriangleObject 객체 안으로 캡슐화되었다. 정점의 배열과 버퍼는 멤버로 선언되었고 생성자에서 배열을 미리 버퍼로 바꾸어 둔다. draw 메서드에서는 색상을 지정하고 정점 배열로 삼각형을 그리기만 한다.

랜더러는 TriangleObject를 멤버로 선언해 놓고 onDrawFrame에서 삼각형과 관련된 그리기는 객체의 draw 메서드로 그린다. 도형들이 굉장히 많아도 코드가 체계적으로 나누어지므로 관리하기 좋고 그리기에 관련된 예비 동작을 객체 수준에서 준비하므로 속도에도 유리하다.

11-3.입체 도형

입체 도형을 그리는 방법도 윈도우즈와 별반 다르지 않다. 양이 좀 늘어날 뿐 질적으로 앞 예제와 크게 다른 점은 없다.

 

Pyramid

package exam.andexam_opengl;

 

import java.nio.*;

 

import javax.microedition.khronos.egl.*;

import javax.microedition.khronos.opengles.*;

 

import android.app.*;

import android.content.*;

import android.opengl.*;

import android.os.*;

import android.view.*;

 

public class Pyramid extends Activity {

     GLSurfaceView mGLSurfaceView;

     public void onCreate(Bundle savedInstanceState) {

          super.onCreate(savedInstanceState);

 

          mGLSurfaceView = new GLSurfaceView(this);

          mGLSurfaceView.setRenderer(new Renderer());

          setContentView(mGLSurfaceView);

     }

 

     protected void onResume() {

          super.onResume();

          mGLSurfaceView.onResume();

     }

 

     protected void onPause() {

          super.onPause();

          mGLSurfaceView.onPause();

     }

 

     class Renderer implements GLSurfaceView.Renderer {

          PyramidObject pyramid;

          public Renderer() {

              pyramid = new PyramidObject();

          }

 

          public void onSurfaceCreated(GL10 gl, EGLConfig config) {

              gl.glClearColor(0.2f,0.2f,0.2f,1);

 

              gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

              gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

              gl.glShadeModel(GL10.GL_FLAT);

              gl.glEnable(GL10.GL_DEPTH_TEST);

          }

 

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

              gl.glViewport(0, 0, width, height);

          }

 

          public void onDrawFrame(GL10 gl) {

              gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

             

              gl.glRotatef(0.3f, 1, 0, 0);

              gl.glRotatef(1.0f, 0, 1, 0);

             

              pyramid.draw(gl);

          }

     }

    

     class PyramidObject {

          float vert[] = {

                   0, 0, -0.8f,              // 중앙

                   0.5f, 0.5f, 0,            // 우상

                   -0.5f, 0.5f, 0,           // 좌상

                   -0.5f, -0.5f, 0,         // 좌하

                   0.5f, -0.5f, 0,           // 우하

                   0.5f, 0.5f, 0,            // 밑면 우상

          };

 

          float color[] = {

              1,1,1,1,                  // 중앙

              0,0,1,1,                  // 우상

              1,0,0,1,                  // 좌상

              1,1,0,1,                  // 좌하

               0,1,0,1,                  // 우하

              1,1,1,1,                  // 밑면 우상

          };

 

          byte index[] = {

              3, 2, 5,              // 밑면1

              4, 3, 5,              // 밑면2

              0, 1, 2,              //12시

              0, 2, 3,              // 9시

              0, 3, 4,              // 6시

              0, 4, 1,              // 3시

          };

          FloatBuffer vertbuf;

          FloatBuffer colorbuf;

          ByteBuffer indexbuf;

         

          public FloatBuffer ArrayToBuffer(float[] ar) {

              ByteBuffer bytebuf = ByteBuffer.allocateDirect(ar.length*4);

              bytebuf.order(ByteOrder.nativeOrder());

              FloatBuffer buf = bytebuf.asFloatBuffer();

              buf.put(ar);

              buf.position(0);

              return buf;

          }

         

          public PyramidObject() {

              vertbuf = ArrayToBuffer(vert);

              colorbuf = ArrayToBuffer(color);

             

              indexbuf = ByteBuffer.allocateDirect(index.length);

              indexbuf.put(index);

              indexbuf.position(0);

          }

 

          public void draw(GL10 gl) {

              gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertbuf);

              gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorbuf);

              gl.glDrawElements(GL10.GL_TRIANGLES, index.length,

                        GL10.GL_UNSIGNED_BYTE, indexbuf);

          }

     }

}

 

 

입체이므로 정점 배열에 z 좌표를 하나 더 넣어 주면 된다. 색상을 입히려면 색상 배열도 지정해야 하고 GL_COLOR_ARRAY 기능도 반드시 켜 주어야 한다. 그리고 입체이므로 깊이 테스트도 반드시 필요하다. 셰이드 모델은 단색으로 지정했다.

vert 배열에는 z 좌표가 하나 더 늘어났다. ES는 사각형을 지원하지 않으므로 밑면의 사각형은 두 개의 삼각형으로 분할해서 그려야 한다. 피라미드를 구성하는 정점을 지정하되 밑면 사각형의 색상이 다르므로 오른쪽 위 좌표를 한번 더 써 주었다. 좌표는 삼각형의 정점과 동일하지만 대응되는 색상이 다르므로 중복된 정점이 하나 더 필요하다.

색상 배열에는 각 정점의 색상을 적는다. 알파값까지 반드시 4개의 요소를 다 지정해야 한다. 각면에 원색을 지정했고 밑면은 흰색으로 지정했다. 인덱스 배열에는 물체를 구성하는 각 삼각형의 정점이 기록되어 있다. 실행 결과는 다음과 같다.

피라미드의 꼭지점이 정면을 보고 있는 상태라 그대로 둬서는 입체인지 확인되지 않는다. 그래서 랜더 모드를 연속으로 지정하고 매번 그리기를 할 때마다 x축 기준으로 0.3도, y축 기준으로 1도만큼 회전시켰다. 얼마나 빨리 회전할 것인가는 장비의 속도에 따라 다르다.

자동 회전은 입체임을 확인해 볼 수 있지만 사용자가 직접 회전시키는 것이 아니므로 물체의 요모 조모를 훓어 보기에는 불편하다. 다음 예제는 사용자의 키보드, 터치 입력을 받아서 물체를 회전시킨다.

 

Rotate

package exam.andexam_opengl;

 

import java.nio.*;

 

import javax.microedition.khronos.egl.*;

import javax.microedition.khronos.opengles.*;

 

import exam.andexam_opengl.Pyramid.*;

 

import android.app.*;

import android.content.*;

import android.opengl.*;

import android.os.*;

import android.view.*;

 

public class Rotate extends Activity {

     MySurface mGLSurfaceView;

     float xAngle, yAngle, zAngle;

     public void onCreate(Bundle savedInstanceState) {

          super.onCreate(savedInstanceState);

 

          mGLSurfaceView = new MySurface(this);

          mGLSurfaceView.setFocusable(true);

          mGLSurfaceView.setFocusableInTouchMode(true);

          mGLSurfaceView.setRenderer(new Renderer());

          mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

          setContentView(mGLSurfaceView);

     }

 

     protected void onResume() {

          super.onResume();

          mGLSurfaceView.onResume();

     }

 

     protected void onPause() {

          super.onPause();

          mGLSurfaceView.onPause();

     }

 

     class MySurface extends GLSurfaceView {

 

          public MySurface(Context context) {

              super(context);

          }

         

          public boolean onKeyDown(int keyCode, KeyEvent event) {

              switch (keyCode) {

              case KeyEvent.KEYCODE_A:

                   yAngle += 2;

                   break;

              case KeyEvent.KEYCODE_D:

                   yAngle -= 2;

                   break;

              case KeyEvent.KEYCODE_W:

                   xAngle += 2;

                   break;

              case KeyEvent.KEYCODE_S:

                   xAngle -= 2;

                   break;

              case KeyEvent.KEYCODE_Q:

                    zAngle += 2;

                   break;

              case KeyEvent.KEYCODE_E:

                   yAngle -= 2;

                   break;

              case KeyEvent.KEYCODE_Z:

                   xAngle = yAngle = zAngle = 0;

                   break;

              }

              if (keyCode == KeyEvent.KEYCODE_A) {

                   yAngle += 2;

              }

 

              requestRender();

              return super.onKeyDown(keyCode, event);    

          }

         

          public boolean onTouchEvent(MotionEvent event) {

              WindowManager wm = (WindowManager)getSystemService(

                        Context.WINDOW_SERVICE);

              Display dis = wm.getDefaultDisplay();

              int mx = (int) (event.getX()/(dis.getWidth()/3));

              int my = (int) (event.getY()/(dis.getHeight()/3));

              int pos = my * 3 + mx;

             

              switch(pos) {

              case 0:

                   zAngle += 2;

                   break;

              case 1:

                   xAngle += 2;

                   break;

              case 2:

                   zAngle -= 2;

                   break;

              case 3:

              case 6:

                   yAngle += 2;

                   break;

              case 4:

                   xAngle = yAngle = zAngle = 0;

                   break;

              case 5:

              case 8:

                   yAngle -= 2;

                   break;

              case 7:

                   xAngle -= 2;

                   break;

              }

             

              requestRender();

              return super.onTouchEvent(event);

          }

     }

    

     class Renderer implements GLSurfaceView.Renderer {

          PyramidObject pyramid;

          public Renderer() {

              pyramid = new PyramidObject();

          }

 

          public void onSurfaceCreated(GL10 gl, EGLConfig config) {

              gl.glClearColor(0.2f,0.2f,0.2f,1);

 

              gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

              gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

              gl.glShadeModel(GL10.GL_SMOOTH);

              gl.glEnable(GL10.GL_DEPTH_TEST);

          }

 

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

              gl.glViewport(0, 0, width, height);

          }

 

          public void onDrawFrame(GL10 gl) {

              gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

             

              gl.glMatrixMode(GL10.GL_MODELVIEW);

              gl.glLoadIdentity();

              gl.glRotatef(xAngle, 1, 0, 0);

              gl.glRotatef(yAngle, 0, 1, 0);

              gl.glRotatef(zAngle, 0, 0, 1);

             

              pyramid.draw(gl);

          }

     }

 

     class PyramidObject {

          float vert[] = {

                   0, 0, -0.8f,              // 중앙

                   0.5f, 0.5f, 0,            // 우상

                   -0.5f, 0.5f, 0,           // 좌상

                   -0.5f, -0.5f, 0,         // 좌하

                   0.5f, -0.5f, 0,           // 우하

                   0.5f, 0.5f, 0,            // 밑면 우상

              };

 

          // 색상값에 알파도 반드시 필요함.

          float color[] = {

              1,1,1,1,                  // 중앙

              0,0,1,1,                  // 우상

              1,0,0,1,                  // 좌상

              1,1,0,1,                  // 좌하

              0,1,0,1,                  // 우하

              1,1,1,1,                  // 밑면 우상

          };

 

          byte index[] = {

              3, 2, 5,              // 밑면1

              4, 3, 5,              // 밑면2

              0, 1, 2,              //12시

              0, 2, 3,              // 9시

              0, 3, 4,              // 6시

              0, 4, 1,              // 3시

          };

          FloatBuffer vertbuf;

          FloatBuffer colorbuf;

          ByteBuffer indexbuf;

         

          public FloatBuffer ArrayToBuffer(float[] ar) {

              ByteBuffer bytebuf = ByteBuffer.allocateDirect(ar.length*4);

              bytebuf.order(ByteOrder.nativeOrder());

              FloatBuffer buf = bytebuf.asFloatBuffer();

              buf.put(ar);

              buf.position(0);

              return buf;

          }

 

          public PyramidObject() {

              vertbuf = ArrayToBuffer(vert);

              colorbuf = ArrayToBuffer(color);

             

               indexbuf = ByteBuffer.allocateDirect(index.length);

              indexbuf.put(index);

              indexbuf.position(0);

          }

 

          public void draw(GL10 gl) {

              gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertbuf);

              gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorbuf);

              gl.glDrawElements(GL10.GL_TRIANGLES, index.length,

                        GL10.GL_UNSIGNED_BYTE, indexbuf);

          }

     }

}

 

 

입력이 있을 때만 다시 그리면 되므로 랜더 모드는 연속으로 할 필요가 없다. 셰이드 모드는 디폴트인 GL_SMOOTH를 받아들이도록 했다. 각 면의 색상이 화려하게 그라데이션 처리된다.

입력을 받으려면 GLSurfaceView를 상속받아 입력 메서드를 재정의해야 하며 키보드 입력을 받으려면 포커스를 반드시 주어야 한다. x, y, z 세 축에 대한 회전 각도를 변수로 정의하고 onDrawFrame에서 각 축에 따라 회전시켰다. 한번 회전해 놓은 것은 다음번 그리기에도 누적적으로 적용되므로 회전하기 전에 모델뷰 행렬을 반드시 리셋해야 함을 주의하자.

키보드 처리 루틴은 아주 쉽다. ad 입력으로 y축, ws 입력으로 x축, qe 입력으로 z축 각도를 증감시키고 requestRender 메서드를 호출하여 다시 그리기만 하면 된다. 터치 처리 루틴은 약간 복잡해 보이는데 화면을 9분할하고 각 분할면의 터치를 입력받아 회전시키는 것이다. 정 중앙을 누르면 리셋된다. 다음 예제는 색상 큐브 육면체를 그리고 터치 드래그로 회전시킨다.

 

ColorCube

package exam.andexam_opengl;

 

import java.nio.*;

 

import javax.microedition.khronos.egl.*;

import javax.microedition.khronos.opengles.*;

 

import android.app.*;

import android.content.*;

import android.opengl.*;

import android.os.*;

import android.view.*;

 

public class ColorCube extends Activity {

     MySurface mGLSurfaceView;

     float xAngle= 45, yAngle = 45;

     float oldx, oldy;

     public void onCreate(Bundle savedInstanceState) {

          super.onCreate(savedInstanceState);

 

          mGLSurfaceView = new MySurface(this);

          mGLSurfaceView.setRenderer(new Renderer());

          mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

          setContentView(mGLSurfaceView);

     }

 

     protected void onResume() {

          super.onResume();

          mGLSurfaceView.onResume();

     }

 

     protected void onPause() {

          super.onPause();

          mGLSurfaceView.onPause();

     }

    

     class MySurface extends GLSurfaceView {

          final static float DRAGSPEED = 5;

          public MySurface(Context context) {

              super(context);

          }

         

          public boolean onTouchEvent(MotionEvent event) {

              float x = event.getX();

             float y = event.getY();

 

             if (event.getAction() == MotionEvent.ACTION_MOVE) {

               float dx = (oldx - x)/DRAGSPEED;

               float dy = (oldy - y)/DRAGSPEED;

            

               yAngle += dx;

               xAngle += dy;

              

                   requestRender();

             }

 

          oldx = x;

          oldy = y;

 

          return true;

          }

     }

 

     class Renderer implements GLSurfaceView.Renderer {

          CubeObject cube;

          public Renderer() {

              cube = new CubeObject();

          }

 

          public void onSurfaceCreated(GL10 gl, EGLConfig config) {

              gl.glClearColor(0.2f,0.2f,0.2f,1);

              gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

              gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

              gl.glEnable(GL10.GL_DEPTH_TEST);

          }

 

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

              gl.glViewport(0, 0, width, height);

          }

 

          public void onDrawFrame(GL10 gl) {

              gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

             

              gl.glMatrixMode(GL10.GL_MODELVIEW);

              gl.glLoadIdentity();

              gl.glRotatef(xAngle, 1, 0, 0);

              gl.glRotatef(yAngle, 0, 1, 0);

 

              cube.draw(gl);

          }

     }

    

     class CubeObject {

          float vert[] = {

                   -0.5f, 0.5f, -0.5f,

                   0.5f, 0.5f, -0.5f,

                   0.5f, -0.5f, -0.5f,

                   -0.5f, -0.5f, -0.5f,

                   -0.5f, 0.5f, 0.5f,

                   0.5f, 0.5f, 0.5f,

                   0.5f, -0.5f, 0.5f,

                   -0.5f, -0.5f, 0.5f,

              };

         

          float color[] = {

                   0,1,1,1,

                   1,1,1,1,

                   1,1,0,1,

                   0,1,0,1,

                   0,0,1,1,

                   1,0,1,1,

                   1,0,0,1,

                   0,0,0,1,

              };

         

          byte index[] = {

                   0,3,1,  1,3,2,

                   1,2,6,  1,6,5,

                   0,1,4,  1,5,4,

                   4,5,7,  7,5,6,

                   0,4,7,  0,7,3,

                   3,7,2,  2,7,6,

              };

         

          FloatBuffer vertbuf;

          FloatBuffer colorbuf;

          ByteBuffer indexbuf;

         

          public FloatBuffer ArrayToBuffer(float[] ar) {

              ByteBuffer bytebuf = ByteBuffer.allocateDirect(ar.length*4);

              bytebuf.order(ByteOrder.nativeOrder());

              FloatBuffer buf = bytebuf.asFloatBuffer();

              buf.put(ar);

              buf.position(0);

              return buf;

          }

 

          public CubeObject() {

              vertbuf = ArrayToBuffer(vert);

              colorbuf = ArrayToBuffer(color);

             

              indexbuf = ByteBuffer.allocateDirect(index.length);

              indexbuf.put(index);

              indexbuf.position(0);

          }

         

          public void draw(GL10 gl) {

               gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertbuf);

              gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorbuf);

              gl.glDrawElements(GL10.GL_TRIANGLES, index.length,

                        GL10.GL_UNSIGNED_BYTE, indexbuf);

          }

     }

}

 

육면체는 사각형 6개로 구성되어 있지만 삼각형으로 쪼개서 그려야 하므로 12개의 삼각형이 필요하다. 정점 배열이 상당히 복잡해 보이는데 다음과 같이 각 정점에 번호를 붙여 두고 좌표를 생각해 보면 쉽게 구할 수 있다.

각 정점의 색상은 색상 큐브에 맞게 할당했다. 제일 안쪽 모서리가 검정색이고 x축으로 빨강, y축으로 파랑, z축으로 초록색이다.

onTouchEvent에서 드래그하여 각도를 계산하는 코드는 지극히 기본적인 코드이다. 매 이동거리만큼 각도를 증감시키되 터치 이동 거리가 각도보다는 좀 과장되어 있으므로 적당히 나누어 천천히 회전하도록 했다.

11-4.텍스처

다음 예제는 육면체에 옥수수 사진 텍스처를 입힌다. 텍스처 이미지는 128크기의 cornpng.png 파일로 준비했다. 압축 포맷의 이미지도 사용 가능하다.

 

TextureCube

package exam.andexam_opengl;

 

import java.io.*;

import java.nio.*;

 

import javax.microedition.khronos.egl.*;

import javax.microedition.khronos.opengles.*;

 

import android.app.*;

import android.content.*;

import android.graphics.*;

import android.opengl.*;

import android.os.*;

import android.view.*;

 

public class TextureCube extends Activity {

     MySurface mGLSurfaceView;

     float xAngle= 45, yAngle = 45;

     public void onCreate(Bundle savedInstanceState) {

          super.onCreate(savedInstanceState);

 

          mGLSurfaceView = new MySurface(this);

          mGLSurfaceView.setRenderer(new Renderer());

          mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

          setContentView(mGLSurfaceView);

     }

 

     protected void onResume() {

          super.onResume();

          mGLSurfaceView.onResume();

     }

 

     protected void onPause() {

          super.onPause();

          mGLSurfaceView.onPause();

     }

    

     class MySurface extends GLSurfaceView {

          final static float DRAGSPEED = 5;

          float oldx, oldy;

          public MySurface(Context context) {

              super(context);

          }

         

          public boolean onTouchEvent(MotionEvent event) {

              float x = event.getX();

             float y = event.getY();

 

             if (event.getAction() == MotionEvent.ACTION_MOVE) {

               float dx = (oldx - x)/DRAGSPEED;

               float dy = (oldy - y)/DRAGSPEED;

            

               yAngle += dx;

               xAngle += dy;

              

                   requestRender();

             }

 

          oldx = x;

          oldy = y;

 

          return true;

          }

     }

 

     class Renderer implements GLSurfaceView.Renderer {

          CubeObject cube;

          public Renderer() {

              cube = new CubeObject();

          }

 

          public void onSurfaceCreated(GL10 gl, EGLConfig config) {

              gl.glClearColor(0.2f,0.2f,0.2f,1);

              gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

              gl.glEnable(GL10.GL_DEPTH_TEST);

              gl.glEnable(GL10.GL_TEXTURE_2D);

              gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

 

              InputStream stream = getResources().openRawResource(R.drawable.cornpng);

              Bitmap bitmap = BitmapFactory.decodeStream(stream);

              try {

                   stream.close();

              } catch (IOException e) { ; }

 

              int[] textures = new int[1];

              gl.glGenTextures(1, textures, 0);

              gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

              gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);

              gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);

              GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

              bitmap.recycle();

          }

 

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

              gl.glViewport(0, 0, width, height);

          }

 

          public void onDrawFrame(GL10 gl) {

              gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

             

              gl.glMatrixMode(GL10.GL_MODELVIEW);

              gl.glLoadIdentity();

              gl.glRotatef(xAngle, 1, 0, 0);

              gl.glRotatef(yAngle, 0, 1, 0);

 

              cube.draw(gl);

          }

     }

    

     class CubeObject {

          float vert[] = {

                   -0.5f, 0.5f, -0.5f,           // 0

                   0.5f, 0.5f, -0.5f,

                   0.5f, -0.5f, -0.5f,

                   -0.5f, -0.5f, -0.5f,

                  

                   -0.5f, 0.5f, 0.5f,            // 4

                   0.5f, 0.5f, 0.5f,

                   0.5f, 0.5f, -0.5f,

                   -0.5f, 0.5f, -0.5f,

 

                   -0.5f, 0.5f, 0.5f,            // 8

                   0.5f, 0.5f, 0.5f,

                   0.5f, -0.5f, 0.5f,

                   -0.5f, -0.5f, 0.5f,

         

                   -0.5f, -0.5f, 0.5f,           // 12

                   0.5f, -0.5f, 0.5f,

                   0.5f, -0.5f, -0.5f,

                   -0.5f, -0.5f, -0.5f,

 

                   0.5f, 0.5f, -0.5f,            // 16

                   0.5f, 0.5f, 0.5f,

                   0.5f, -0.5f, 0.5f,

                   0.5f, -0.5f, -0.5f,

 

                   -0.5f, 0.5f, -0.5f,           // 20

                   -0.5f, 0.5f, 0.5f,

                   -0.5f, -0.5f, -0.5f,

                    -0.5f, -0.5f, 0.5f,

          };

         

         

          float text[] = {

                   0,1,1,1,1,0,0,0,

                   0,1,1,1,1,0,0,0,

                   0,1,1,1,1,0,0,0,

                   0,1,1,1,1,0,0,0,

                   0,1,1,1,1,0,0,0,

                   0,1,1,1,1,0,0,0,

          };

         

          byte index[] = {

                   0,3,1,  1,3,2,

                   4,7,6,  4,6,5,

                   8,11,10,    8,10,9,

                   12,14,15,  12,13,14,

                   16,18,19,  16,18,17,

                   20,23,22,  20,21,23, 

          };

         

          FloatBuffer vertbuf;

          FloatBuffer textbuf;

          ByteBuffer indexbuf;

         

          public FloatBuffer ArrayToBuffer(float[] ar) {

              ByteBuffer bytebuf = ByteBuffer.allocateDirect(ar.length*4);

              bytebuf.order(ByteOrder.nativeOrder());

              FloatBuffer buf = bytebuf.asFloatBuffer();

              buf.put(ar);

              buf.position(0);

              return buf;

          }

 

          public CubeObject() {

              vertbuf = ArrayToBuffer(vert);

              textbuf = ArrayToBuffer(text);

 

               indexbuf = ByteBuffer.allocateDirect(index.length);

              indexbuf.put(index);

              indexbuf.position(0);

          }

         

          public void draw(GL10 gl) {

              gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertbuf);

              gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textbuf);

              gl.glDrawElements(GL10.GL_TRIANGLES, index.length,

                        GL10.GL_UNSIGNED_BYTE, indexbuf);

          }

     }

}

 

텍스처로 표면을 덮을 것이므로 색상 배열은 제외했다. 물론 색상 배열과 함께 텍스처를 쓸 수도 있다. 색상 큐브는 각 번호에 따라 정점을 정의해 놓고 공유하면 되지만 텍스처를 입힐 때는 같은 정점이라도 소속되는 면에 따라 텍스처 좌표가 달라지므로 각 면에 따라 정점을 일일이 정의해야 한다. 그리고 대응되는 정점에 대해 이미지의 어떤 부분이 맵핑될 것인지 좌표도 일일이 지정해야 한다. 정점의 번호를 다음과 같이 새로 부여했다.

전부 왼쪽 위에서부터 시계 방향으로 번호를 매김으로써 텍스처와 대응되는 지점을 일치시켰다. 실행 결과는 다음과 같다.

옥수수 이미지가 나름 먹음직스럽다. 정점을 일일이 배열로 만드느라 이 예제 작성에 거의 2시간이 걸렸다.

11-5.조명

다음 예제는 육면체에 주변광과 분산광을 비춘다.

 

Lighting

package exam.andexam_opengl;

 

import java.nio.*;

 

import javax.microedition.khronos.egl.*;

import javax.microedition.khronos.opengles.*;

 

import exam.andexam_opengl.TextureCube.*;

 

import android.app.*;

import android.content.*;

import android.opengl.*;

import android.os.*;

import android.view.*;

 

public class Lighting extends Activity {

     MySurface mGLSurfaceView;

     float xAngle, yAngle;

     public void onCreate(Bundle savedInstanceState) {

          super.onCreate(savedInstanceState);

 

          mGLSurfaceView = new MySurface(this);

          mGLSurfaceView.setRenderer(new Renderer());

          mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

          setContentView(mGLSurfaceView);

     }

 

     protected void onResume() {

          super.onResume();

          mGLSurfaceView.onResume();

     }

 

     protected void onPause() {

          super.onPause();

          mGLSurfaceView.onPause();

     }

 

     class MySurface extends GLSurfaceView {

          final static float DRAGSPEED = 5;

          float oldx, oldy;

          public MySurface(Context context) {

              super(context);

          }

         

          public boolean onTouchEvent(MotionEvent event) {

              float x = event.getX();

             float y = event.getY();

 

             if (event.getAction() == MotionEvent.ACTION_MOVE) {

               float dx = (oldx - x)/DRAGSPEED;

               float dy = (oldy - y)/DRAGSPEED;

            

               yAngle += dx;

               xAngle += dy;

              

                   requestRender();

             }

 

          oldx = x;

          oldy = y;

 

          return true;

          }

     }

    

     class Renderer implements GLSurfaceView.Renderer {

          CubeObject cube;

          FloatBuffer AmbientBuffer;

          FloatBuffer DiffuseBuffer;

          FloatBuffer lightPosBuffer;

 

          public Renderer() {

              cube = new CubeObject();

          }

 

          public void onSurfaceCreated(GL10 gl, EGLConfig config) {

              gl.glClearColor(0.2f,0.2f,0.2f,1);

 

              gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

              gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);

              gl.glShadeModel(GL10.GL_FLAT);

              gl.glEnable(GL10.GL_DEPTH_TEST);

 

              gl.glEnable(GL10.GL_LIGHTING);

              float[] Ambient = {0.5f, 0.5f, 0.5f, 1.0f};

              float[] Diffuse = {0.5f, 0.5f, 0.5f, 1.0f};

              float[] lightPos = {0.0f, 0.0f, -1.0f, 1.0f};

              ByteBuffer bytebuf = ByteBuffer.allocateDirect(Ambient.length * 4);

              bytebuf.order(ByteOrder.nativeOrder());

              AmbientBuffer = bytebuf.asFloatBuffer();

              AmbientBuffer.put(Ambient);

              AmbientBuffer.position(0);

             

              bytebuf = ByteBuffer.allocateDirect(Diffuse.length * 4);

              bytebuf.order(ByteOrder.nativeOrder());

              DiffuseBuffer = bytebuf.asFloatBuffer();

              DiffuseBuffer.put(Diffuse);

              DiffuseBuffer.position(0);

             

              bytebuf = ByteBuffer.allocateDirect(lightPos.length * 4);

              bytebuf.order(ByteOrder.nativeOrder());

              lightPosBuffer = bytebuf.asFloatBuffer();

              lightPosBuffer.put(lightPos);

              lightPosBuffer.position(0);

 

              gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT, AmbientBuffer);

              gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE, DiffuseBuffer);

              gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, lightPosBuffer);

              gl.glEnable(GL10.GL_LIGHT0);

          }

 

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

              gl.glViewport(0, 0, width, height);

          }

 

          public void onDrawFrame(GL10 gl) {

              gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

             

              gl.glMatrixMode(GL10.GL_MODELVIEW);

              gl.glLoadIdentity();

              gl.glRotatef(xAngle, 1, 0, 0);

              gl.glRotatef(yAngle, 0, 1, 0);

             

              cube.draw(gl);

          }

     }

    

     class CubeObject {

          float vert[] = {

                   -0.5f, 0.5f, -0.5f,           // 0

                   0.5f, 0.5f, -0.5f,

                   0.5f, -0.5f, -0.5f,

                   -0.5f, -0.5f, -0.5f,

                  

                   -0.5f, 0.5f, 0.5f,            // 4

                   0.5f, 0.5f, 0.5f,

                   0.5f, 0.5f, -0.5f,

                   -0.5f, 0.5f, -0.5f,

 

                   -0.5f, 0.5f, 0.5f,            // 8

                   0.5f, 0.5f, 0.5f,

                   0.5f, -0.5f, 0.5f,

                   -0.5f, -0.5f, 0.5f,

         

                   -0.5f, -0.5f, 0.5f,           // 12

                   0.5f, -0.5f, 0.5f,

                   0.5f, -0.5f, -0.5f,

                   -0.5f, -0.5f, -0.5f,

 

                   0.5f, 0.5f, -0.5f,            // 16

                   0.5f, 0.5f, 0.5f,

                   0.5f, -0.5f, 0.5f,

                   0.5f, -0.5f, -0.5f,

 

                   -0.5f, 0.5f, -0.5f,           // 20

                   -0.5f, 0.5f, 0.5f,

                   -0.5f, -0.5f, -0.5f,

                   -0.5f, -0.5f, 0.5f,

          };

         

          float normal[] = {

                   0.0f, 0.0f, -1.0f,

                   0.0f, 0.0f, -1.0f,

                   0.0f, 0.0f, -1.0f,

                   0.0f, 0.0f, -1.0f,

 

                   0.0f, 1.0f, 0.0f,

                   0.0f, 1.0f, 0.0f,

                   0.0f, 1.0f, 0.0f,

                   0.0f, 1.0f, 0.0f,

 

                   0.0f, 0.0f, 1.0f,

                   0.0f, 0.0f, 1.0f,

                   0.0f, 0.0f, 1.0f,

                   0.0f, 0.0f, 1.0f,

 

                   0.0f, -1.0f, 0.0f,

                   0.0f, -1.0f, 0.0f,

                   0.0f, -1.0f, 0.0f,

                   0.0f, -1.0f, 0.0f,

 

                   1.0f, 0.0f, 0.0f,

                   1.0f, 0.0f, 0.0f,

                   1.0f, 0.0f, 0.0f,

                   1.0f, 0.0f, 0.0f,

 

                   -1.0f, 0.0f, 0.0f,

                   -1.0f, 0.0f, 0.0f,

                   -1.0f, 0.0f, 0.0f,

                   -1.0f, 0.0f, 0.0f,

          };

         

          byte index[] = {

                   0,3,1,  1,3,2,

                   4,7,6,  4,6,5,

                   8,11,10,    8,10,9,

                   12,14,15,  12,13,14,

                   16,18,19,  16,18,17,

                   20,23,22,  20,21,23, 

          };

         

          FloatBuffer vertbuf;

          FloatBuffer normalbuf;

          ByteBuffer indexbuf;

         

          public FloatBuffer ArrayToBuffer(float[] ar) {

              ByteBuffer bytebuf = ByteBuffer.allocateDirect(ar.length*4);

              bytebuf.order(ByteOrder.nativeOrder());

              FloatBuffer buf = bytebuf.asFloatBuffer();

              buf.put(ar);

              buf.position(0);

              return buf;

          }

 

          public CubeObject() {

              vertbuf = ArrayToBuffer(vert);

              normalbuf = ArrayToBuffer(normal);

             

              indexbuf = ByteBuffer.allocateDirect(index.length);

              indexbuf.put(index);

              indexbuf.position(0);

          }

         

          public void draw(GL10 gl) {

              gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertbuf);

              gl.glNormalPointer(GL10.GL_FLOAT, 0, normalbuf);

              gl.glDrawElements(GL10.GL_TRIANGLES, index.length,

                        GL10.GL_UNSIGNED_BYTE, indexbuf);

          }

     }

}

 

 

조명이 제대로 비추기 위해서는 각 정점에 대해 법선을 지정해야 한다. 각 면에 대해 바깥쪽으로 단위 길이의 법선을 지정했는데 의도한대로 나오지 않았다.

와인딩의 문제는 아닌 것 같고 법선을 잘 못 준 것 같은데 아직 문제를 정확하게 파악하지 못했다. 좀 더 연구가 필요한 부분이다.