Разработка 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 будет намного более функционален и сложен, но я все хорошо прокомментирую для Вас:
/*http://esate.ru, Freaky_Brainstorm(Lipka Aleh)*/

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:
/*http://esate.ru, Freaky_Brainstorm(Lipka Aleh)*/

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:
/*http://esate.ru, Freaky_Brainstorm(Lipka Aleh)*/

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 принимает два параметра:
/*http://esate.ru, Freaky_Brainstorm(Lipka Aleh)*/

public Background(float width, float height)
Эти параметры нужны нам для того, что бы мы могли(в будущем) корректно работать с ним и для того, что бы "растянуть" его.

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


На этом все.
В следующей статье мы поговорим об освещении, нормалях и нарисуем куб. Куб всегда интереснее плоскости.
Файлы:
OGLPart2.7z (2.65 МБ)
1.5324       2300        25.10.2015 23:47:21        7

0.2868  
27.10.2015 10:10:07
Интересно было читать. с java пока не работал

зона 2D рисует закрашенный фон?
    //Начало зоны 2D

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


вместо него можно использовать glClearColor? или с какой-то целью так?
Ссылка 0.2868  
0.0011  
27.10.2015 10:24:32
Да, эта зона в данном примере рисует плоскость на весь экран светло-серого цвета.

Такой подход на много лучше чем использование glClearColor. Вы получаете экранные координаты и можете располагать элементы UI и прочее + на плоскость можно натянуть текстуру. Плоскость так же может отражать свет.
0  
27.10.2015 10:38:05
понятно. а
элементы UI и прочее
т.е. свои элементы или можно что-то системное повесить, чтобы не изобретать велосипед
0  
27.10.2015 10:47:28
т.е. свои элементы или можно что-то системное повесить, чтобы не изобретать велосипед
Системные можно добавлять и без этого, путем перетаскивания их на активити из визуального редактора.

С помощью такого разделения мы просто получаем возможность использовать более продвинутый background, ограниченный в своем функционале только возможностями OpenGL 1.0.
0  
27.10.2015 12:42:59
спасибо, понял
0.3544  
27.10.2015 15:16:11
Хорошо получается :)
Я бы еще в конце каждой статьи добавлял изображение с результатами работы.
Понимаю, что не всегда оно будет впечатляющим или красочным, но без него чего-то не хватает, особенно если нет возможности попробовать сразу собрать приложение. (но это исключительно по моему скромному мнению :)
Ссылка 0.3544  
0  
27.10.2015 23:30:28
Спасибо за совет. Отредактировал :)
^