Разработка OpenGL приложения под Android. Часть 2. Матрицы.

Доброго времени суток.

Это третья часть цикла статей “Разработка OpenGL приложения под Android”.

Предыдущие статьи:

Разработка OpenGL приложения под Android. Часть 0. Введение.

Разработка OpenGL приложения под Android. Часть 1. Знакомство.

В прошлой статье мы создали, наверное, самое простое приложение, использующее мощь OpenGL, которое только может быть.

Это всего лишь микроскопическая часть возможностей этой библиотеки. В этой главе я хочу поговорить с Вами о матрицах.

Как пишут во всех уроках и статьях на данную тему, скажу Вам, что, действительно, необходимости знать все свойства матриц у Вас нет, но их понимание способствовало бы скорейшему усвоению всех процессов OpenGL. С помощью матриц происходят смещение, поворот и прочие искажения пространства.

В OpenGL 1.0(именно эту версию мы пока используем) есть три типа матриц: GL_MODELVIEW, GL_PROJECTION и GL_TEXTURE.

Остановимся на каждой из них немного подробнее.

  • GL_MODELVIEW

Это матрица “активной камеры”. В ней объединены две матрицы - матрица модели и матрица вида.

/ Если Вы новичок, то это довольно размытое определение, я понимаю, но, скорее всего, скоро Вы все поймете. /

Она служит для отрисовки объемных объектов сцены.

/ Сцена - это все, что мы видим в нашем GLSurfaceView, т.е. в пространстве OpenGL. /

Говоря другими словами, GL_MODELVIEW нужен нам когда мы хотим работать с 3D.

  • GL_PROJECTION

Это матрица проекции. Ее название говорит само за себя. Эта матрица служит для проецирования 3D пространства или его участков в 2D.

Если есть сложности с пониманием того, что же такое проекция - ссылка на википедию.

  • GL_TEXTURE

Последняя из трех матриц по порядку и по количеству использования(мое личное мнение).

Данная матрица используется для регулировки накладывания текстур на полигоны.

/* Лично мне еще ни разу не довелось ее использовать. */

Для задания необходимой матрицы в OpenGL используется функция glMatrixMode(), принимающая в себя описанные выше константы.

Этой информации пока будет достаточно. Все остальное мы постараемся разобрать по мере написания кода.

Сегодня мы напишем с вами программу, которая, как мне кажется, не раз Вам пригодится в будущем. Программу, где фон будет отрисован в 2D и иметь свои(экранные) координаты, а объект отрисуем в 3D(мировых) координатах.

Для начала мы повторим манипуляции из первой части серии и создадим GLSurfaceView и подключим его к проекту.

Затем Вам необходимо снова создать класс OpenGLRenderer и подключить его к GLSurfaceView.

На этот раз базовый код OpenGLRenderer будет намного более функционален и сложен, но я все хорошо прокомментирую для Вас:

package com.freakybrainstorm.oglpart2;

import android.opengl.GLSurfaceView;
import android.opengl.GLU;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class OpenGLRenderer implements GLSurfaceView.Renderer {

   private float aspect, width, height;
   private Background background;
   private Object3D object3D;

   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {}

   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      //Создаем экземпляр классов Background и Object3D
      background = new Background(width, height);
      object3D = new Object3D();

      //Переменная aspect будет хранить в себе значение соотношения сторон
      aspect = (float)width / (float)height;
      //Устанавливаем область отрисовки
      gl.glViewport(0, 0, width, height);

      //Выбираем матрицу проекции
      gl.glMatrixMode(GL10.GL_PROJECTION);
      //Умножаем ее на единичную
      gl.glLoadIdentity();
      //Выбираем матрицу вида
      gl.glMatrixMode(GL10.GL_MODELVIEW);
      //Ее тоже умножаем на единичную
      gl.glLoadIdentity();

      //Это все делается для того, чтобы при изменении размера экрана
      //при повороте устройства, у нас ничего не "сломалось"

      //Записываем в переменные значения высоты и ширины экрана
      this.width = (float) width; this.height = (float)height;
   }

   @Override
   public void onDrawFrame(GL10 gl) {
      //Очищаем буферы цвета и глубины
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
      //Выбираем матрицу проекции
      gl.glMatrixMode(GL10.GL_PROJECTION);
      //Умножаем на единичную
      gl.glLoadIdentity();
      //Устанавливаем ортогональную проекцию
      gl.glOrthof(0.0f, this.width, 0.0f, this.height, -1.0f, 1.0f);
      //Отключаем тест глубины
      gl.glDisable(GL10.GL_DEPTH_TEST);
      //Отключаем запись в буфер глубины
      gl.glDepthMask(false);
      //Выбираем матрицу вида
      gl.glMatrixMode(GL10.GL_MODELVIEW);
      //Умножаем на единичную
      gl.glLoadIdentity();
      //Начало зоны 2D

      //Задаем цвет фона
      gl.glColor4f(.7f, .7f, .7f, 1.0f);
      //Запускаем прорисовку
      background.Draw(gl);

      //Конец зоны 2D
      //Включаем запись в буфер глубины
      gl.glDepthMask(true);
      //Включаем тест глубины
      gl.glEnable(GL10.GL_DEPTH_TEST);
      //Выбираем матрицу проекции
      gl.glMatrixMode(GL10.GL_PROJECTION);
      //Умножаем на единичную матрицу
      gl.glLoadIdentity();
      //Устанавливаем область видимости. Об этом ниже
      GLU.gluPerspective(gl, 45.0f, aspect, 0.1f, 1000.0f);
      //Выбираем матрицу вида
      gl.glMatrixMode(GL10.GL_MODELVIEW);
      //Умножаем на единичную матрицу
      gl.glLoadIdentity();
      //Устанавливаем точку и направление камеры
      GLU.gluLookAt(gl, 8.0f, 12.0f, 10.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
      //Начало зоны 3D

      //Задаем цвет объекта
      gl.glColor4f(.1f, .1f, .1f, 1.0f);
      //Запускаем прорисовку
      object3D.Draw(gl);

      //Конец зоны 3D
      //Умножаем на единичную матрицу
      gl.glLoadIdentity();
   }
}

Код функции onDrawFrame() я когда-то нашел на просторах интернета, но он прекрасно работает и в будущем мы его дополним и преобразуем еще лучше.

Каждая строка кода прокомментирована и, я думаю, у Вас не возникнет много вопросов.

Единственное, что я хотел бы пояснить это функции glOrthof(), GLU.gluPerspective() и gluLookAt().

  • glOrthof()

При помощи данной функции мы устанавливаем матрицу проекции в левосторонней системе координат. Другими словами, определяем систему координат так, как нам удобно.

В нашем случае точка 0,0 расположена в верхнем левом углу.

  • GLU.gluPerspective()

Стандартная функция установки перспективы. Принимает экземпляр OpenGL, угол обзора(45 градусов это стандарт), соотношение сторон экрана, ближняя плоскость отсечения и дальняя плоскость отсечения.

/ Плоскости отсечения ограничивают зону видимости. /

  • gluLookAt()

Эта функция установки точки обзора камеры(функция библиотеки GLU). Ее параметры достаточно просты. Первый параметр это экземпляр OpenGL, со второго по четвертый - точка положения камеры, с пятого по седьмой - точка, в которую направлена камера и последние три параметра определяют верх камеры.

Последние три параметра практически всегда будут 0, 1, 0.

/* Я выставил такие координаты положения камеры, что бы наглядно показать вращение матрицы вида и статичность матрицы проекции. */

С первоначальным скелетом класса OpenGLRenderer разобрались. Теперь дело за фоном и объектом.

Для их реализации мы создадим два новых класса: Background и Object3D.

  • Код класса Background:
package com.freakybrainstorm.oglpart2;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;

public class Background {

   private FloatBuffer vertexBuffer;
   private float vertices[];

   public Background(float width, float height) {
      this.vertices = new float[]{
           0.0f,    0.0f,    0.0f,
           0.0f,    height,   0.0f,
           width,    0.0f,    0.0f,
           width,    height,   0.0f
      };

      ByteBuffer byteBuffer = ByteBuffer.allocateDirect(vertices.length * 4);
      byteBuffer.order(ByteOrder.nativeOrder());
      vertexBuffer = byteBuffer.asFloatBuffer();
      vertexBuffer.put(vertices);
      vertexBuffer.position(0);
   }

   public void Draw(GL10 gl) {
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
   }
}
  • Код класса Object3D:
package com.freakybrainstorm.oglpart2;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;

public class Object3D {

   private FloatBuffer vertexBuffer;

   private float vertices[] = {
        -1.0f,  -1.0f,  -1.0f,
        1.0f,   -1.0f,  -1.0f,
        -1.0f,  1.0f,   -1.0f,
        1.0f,   1.0f,   -1.0f
   };

   public Object3D() {
      ByteBuffer byteBuffer = ByteBuffer.allocateDirect(vertices.length * 4);
      byteBuffer.order(ByteOrder.nativeOrder());
      vertexBuffer = byteBuffer.asFloatBuffer();
      vertexBuffer.put(vertices);
      vertexBuffer.position(0);
   }

   public void Draw(GL10 gl) {
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
   }
}

Я намерено не стал комментировать код этих двух классов. Они практически идентичны коду из прошлой части цикла.

Поясню только один момент, хотя и он, скорее всего, будет Вам понятен.

Конструктор Background принимает два параметра:

public Background(float width, float height)

Эти параметры нужны нам для того, что бы мы могли(в будущем) корректно работать с ним и для того, что бы “растянуть” его.

Результат работы нашего приложения ниже. Визуально он не отличим от предыдущего, но теперь у нас есть гораздо больше возможностей для творчества.

На этом все.

В следующей статье мы поговорим об освещении, нормалях и нарисуем куб. Куб всегда интереснее плоскости.