Разработка 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)
Эти параметры нужны нам для того, что бы мы могли(в будущем) корректно работать с ним и для того, что бы “растянуть” его.
Результат работы нашего приложения ниже. Визуально он не отличим от предыдущего, но теперь у нас есть гораздо больше возможностей для творчества.

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