LXF165-166: Android
Olkol (обсуждение | вклад) (→Подключаемся к Матрице) |
Olkol (обсуждение | вклад) (→Квадрат превращается в куб) |
||
Строка 521: | Строка 521: | ||
} | } | ||
− | + | [[Файл:LXF166.code_android.cu_opt.jpeg | |thumb|400px| Разноцветный вращающийся куб.]] | |
public void onDrawFrame(GL10 gl) { | public void onDrawFrame(GL10 gl) { | ||
Строка 545: | Строка 545: | ||
Теперь в методе onDrawFrame() нужно умножить эту матрицу на матрицу обзора камеры (vMatrix) и на проекционную матрицу (projMatrix). Это означает, что нужно выделить строки и умножать каждую из них по очереди для получения объединенной матрицы, но по сути мы делаем здесь то же, что и раньше (объединяем различные матрицы в одну, чтобы применить ее к нашей фигуре), только с еще одной матрицей. Потом мы рисуем наш куб. | Теперь в методе onDrawFrame() нужно умножить эту матрицу на матрицу обзора камеры (vMatrix) и на проекционную матрицу (projMatrix). Это означает, что нужно выделить строки и умножать каждую из них по очереди для получения объединенной матрицы, но по сути мы делаем здесь то же, что и раньше (объединяем различные матрицы в одну, чтобы применить ее к нашей фигуре), только с еще одной матрицей. Потом мы рисуем наш куб. | ||
− | + | ||
У нас должно получиться нечто похожее на куб, но его грани выглядят немного странно. Чтобы они смотрелись более солидно, добавьте еще две строки в метод onSurfaceCreated() после вызова glClearColor(): | У нас должно получиться нечто похожее на куб, но его грани выглядят немного странно. Чтобы они смотрелись более солидно, добавьте еще две строки в метод onSurfaceCreated() после вызова glClearColor(): | ||
Версия 02:58, 1 ноября 2018
|
|
|
Android
Программирование: Работа с графикой при помощи библиотеки OpenGL
Содержание |
Создаем объем с OpenGL
Джульетта Кемп еще помнит времена, когда в фотоаппаратах была настоящая пленка. Сейчас она делает гораздо поболее снимков, чем тогда.
OpenGL (Open Graphics Library – открытая графическая библиотека) – широко используемая спецификация кроссплатформенного API для создания двумерной и трехмерной графики. Путем введения стандартного набора возможностей и интерфейса она упрощает работу с различными аппаратными платформами и 3D-ускорителями для разработчиков. Существуют привязки и реализации OpenGL для огромного количества языков программирования, и, к счастью, в Android API есть встроенная поддержка OpenGL. С первого релиза Android поддерживаются OpenGL ES 1.0 и 1.1, с Android 2.2 (API уровня 8) поддерживается OpenGL ES 2.0.
На этом уроке мы будем пользоваться OpenGL ES 2.0, так как сегодня почти на 85 % устройств используется Android 2.2 и выше, но если вам важна обратная совместимость с более старыми версиями, можно проверять уровень API в коде и при необходимости использовать OpenGL ES 1.1.
В этой статье я буду предполагать, что вы никогда не работали с OpenGL или с ее реализацией в Android API, и объясню некоторые основные идеи. Мы начнем со статичной двумерной фигуры, затем перейдем к вращающемуся кубу. На следующем уроке мы продолжим работу с этим примером, добавив в него работу с сенсорным экраном.
Начинаем знакомство с OpenGL
Поддержка OpenGL встроена в Android по умолчанию, и чтобы воспользоваться этим в своем проекте, ничего особенного делать не нужно. Однако надо добавить в манифест строку (поместите ее после uses-sdk), говорящую о том, что для запуска на устройстве приложение должно поддерживать OpenGL2.0:
<uses-feature android:glEsVersion=”0x00020000” android:required=”true” />
Основное Занятие [Activity] приложения не требует пояснений:
public class CubeInSpaceActivity extends Activity {
private GLSurfaceView glView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
glView = new GLSurfaceView(this);
glView.setEGLContextClientVersion(2);
glView.setRenderer(new CISGLRenderer());
setContentView(glView);
}
@Override
protected void onResume() {
super.onResume();
glView.onResume();
}
@Override
protected void onPause() {
super.onPause();
glView.onPause();
}
}
onCreate() создает используемый далее GLSurfaceView и контекст OpenGL ES 2.0 для него, и настраивает Renderer, который мы сейчас напишем. Для приостановки и возобновления приложения важно использовать методы Представления onPause() и onResume(). OpenGL великолепен, но потребляет много ресурсов, и нам незачем, чтобы он продолжал работать, пока пользователь не смотрит на нашу шикарную графику.
Большая часть работы выполняется классом CISGLRenderer; вот его первая версия, в ней задается только цвет фона:
public class CISGLRenderer implements GLSurfaceView.
Renderer {
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
GLES20.glClearColor(61f/255, 89f/255, 171f/255, 1.0f);
}
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
}
Для реализации GLSurfaceView.Renderer, нужно реализовать три следующих метода. onSurfaceCreated() вызывается один раз при создании и задает параметры объекта; onDrawFrame() вызывается при каждой перерисовке кадра (постоянно); а onSurfaceChanged() вызывается при геометрических изменениях в Представлении (чаще всего при изменении ориентации экрана).
Обратите внимание, что у каждого из этих методов есть параметр GL10; так как мы пользуемся GLES20, использующим статические методы, этот параметр остается незадействованным (если вы хотите реализовать поддержку и для GL10, можете его задействовать).
glClearColor устанавливает цвет фона, который будет применяться каждый раз при вызове glClear – здесь это приятный кобальтово-синий. Параметры метода – красная (R), зеленая (G), синяя (B) составляющая и альфа (она управляет прозрачностью), каждый от 0 до 1. Для преобразования из стандартных 255 RGB можно, как и я, разделить стандартное значение на 255, или разделить вручную и подставить сюда результат.
Единственный аргумент glClear() – битовая маска очистки буфера: здесь это цвет и глубина (подробнее о буфере глубины позже, когда дойдем до трехмерных фигур).
Наконец, в методе onSurfaceChanged() мы задаем размеры прямоугольной области просмотра в соответствии с шириной и высотой поверхности Представления.
Но это только фон. Давайте определим квадрат, который нарисуем на нем, с помощью класса Square:
public class Square {
private FloatBuffer squareBuffer;
float vertices[] = {
-0.5f, -0.5f, 0.0f, //bottom left
0.5f, -0.5f, 0.0f, //bottom right
-0.5f, 0.5f, 0.0f, //top left
0.5f, 0.5f, 0.0f //top right
};
public Square() {
ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
vbb.order(ByteOrder.nativeOrder());
squareBuffer = vbb.asFloatBuffer();
squareBuffer.put(vertices);
squareBuffer.position(0);
}
}
В массиве vertices задается расположение каждого угла (вершины) в координатах X, Y и Z (наш квадрат двумерный, поэтому координата Z везде равна нулю). Точка отсчета системы координат OpenGL находится в центре экрана, как показано на рисунке (Z положительные) и уходит вглубь экрана (Z отрицательные).
Создаем ByteBuffer
При задании параметров квадрата сначала создается байтовый буфер ByteBuffer, с помощью четырех байтов для каждой точки в обычном порядке байтов устройства. Затем с его помощью создается буфер с плавающей точкой squareBuffer; помещаем туда вершины и задаем начальную позицию на первой координате.
Вернемся в CISGLRenderer.java, чтобы настроить вершинный и фрагментный шейдеры. Вершинный шейдер управляет размещением и прорисовкой вершин (углов) фигур в OpenGL. Фрагментный шейдер управляет тем, что рисуется между вершинами. Для их создания нам понадобится объект Strings, который будет передан OpenGL:
private final String vertexShaderCode =
“attribute vec4 vPosition; \n” +
“void main(){ \n” +
“ gl_Position = vPosition; \n” +
“} \n”;
private final String fragmentShaderCode =
“precision mediump float; \n” +
“void main(){ \n” +
“ gl_FragColor = vec4 (0.63671875, 0.76953125, 0.22265625, 1.0); \n” +
“} \n”;
private Square square;
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
GLES20.glClearColor(0.5f, 0.5f, 05.f, 1.0f);
square = new Square();
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
}
private int loadShader(int type, String shaderCode) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
Чтобы создать шейдер, нужно загрузить код в виде строки String и скомпилировать его, как в методе loadShader().
Для прорисовки в OpenGL ES 2.0 используется Program; это объект, который берет шейдеры (исполняемый код) и связывает их друг с другом, чтобы с их помощью нарисовать объекты. Поэтому нам нужно создать программу и связать с ней шейдеры:
private int program; private int positionHandle;
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
[ ... ]
program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
positionHandle = GLES20.glGetAttribLocation(program, “vPosition”);
}
В этом коде создается новый пустой объект Program, с ним связываются оба наших шейдера, затем метод glLinkProgram() создает необходимый исполняемый код.
Наконец, в последней строке мы получаем дескриптор переменной vPosition в коде вершинного шейдера. Это позволяет нам передать расположение каждой вершины из нашего кода для Android в программу OpenGL.
Задав параметры программы OpenGL, добавьте следующие строки в метод onDrawFrame(), чтобы нарисовать квадрат:
GLES20.glUseProgram(program);
square.draw(positionHandle);
а это __draw()__ method to __Square__:
public void draw(int positionHandle) {
GLES20.glVertexAttribPointer(positionHandle, 3, GLES20. GL_FLOAT, false, 0, squareBuffer);
GLES20.glEnableVertexAttribArray(positionHandle);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, vertices.length/3);
}
glVertexAttribPointer() определяет массив данных вершин. positionHandle – индекс отображаемой вершины, которая здесь состоит из трех компонентов (X, Y, Z). Тип данных – GL_FLOAT, значения не должны нормализоваться (false), между значениями массива вершин нет смещения (0), а информация о вершине находится в squareBuffer.
- Метамодернизм в позднем творчестве В.Г. Сорокина
- ЛитРПГ - последняя отрыжка постмодерна
- "Ричард III и семиотика"
- 3D-визуализация обложки Ridero создаем обложку книги при работе над самиздатом.
- Архитектура метамодерна - говоря о современном искусстве, невозможно не поговорить об архитектуре. В данной статье будет отмечено несколько интересных принципов, характерных для построек "новой волны", столь притягательных и скандальных.
- Литература
- Метамодерн
- Рокер-Прометей против изначального зла в «Песне про советскую милицию» Вени Дркина, Автор: Нина Ищенко, к.ф.н, член Союза Писателей ЛНР - перепубликация из журнала "Топос".
- Как избавиться от комаров? Лучшие типы ловушек.
- Что делать если роблокс вылетает на windows
- Что делать, если ребенок смотрит порно?
- Почему собака прыгает на людей при встрече?
- Какое масло лить в Задний дифференциал (мост) Visco diff 38434AA050
- О чем может рассказать хвост вашей кошки?
- Верветки
- Отчетность бюджетных учреждений при закупках по Закону № 223-ФЗ
- Срок исковой давности как правильно рассчитать
- Дмитрий Патрушев минсельхоз будет ли преемником Путина
- Кто такой Владислав Поздняков? Что такое "Мужское Государство" и почему его признали экстремистским в России?
- Как правильно выбрать машинное масло в Димитровграде?
- Как стать богатым и знаменитым в России?
- Почему фильм "Пипец" (Kick-Ass) стал популярен по всему миру?
- Как стать мудрецом?
- Как правильно установить FreeBSD
- Как стать таким как Путин?
- Где лучше жить - в Димитровграде или в Ульяновске?
- Почему город Димитровград так называется?
- Что такое метамодерн?
- ВАЖНО! Временное ограничение движения автотранспортных средств в Димитровграде
- Тарифы на электроэнергию для майнеров предложено повысить
glEnableVertexAttribArray() активирует массив, а glDrawArrays() превращает массив данных в визуализированные геометрические примитивы. Мы пользуемся типом GL_TRIANGLE_STRIP, начиная индекс с нуля, и визуализируем четыре вершины, так как в массиве vertices для каждой вершины есть три значения (X, Y, Z). Скомпилируйте и запустите программу, и вы должны увидеть свою фигуру... однако это не совсем квадрат?
Делаем квадрат квадратным
Проблема с неквадратным квадратом появилась из-за предположения OpenGL, что область просмотра представляет собой квадрат, а на устройствах с Android экран обычно совсем не квадратный. Чтобы это исправить, нам понадобятся проекционная матрица и матрица обзора камеры, с которыми мы правильно вычислим координаты и привяжем их к экрану вашего устройства. Более подробно о матрицах написано во врезке; проекционная матрица по сути связывает «идеальный квадрат» OpenGL с реальным экраном, а матрица обзора камеры представляет визуализируемые объекты так, как их видит камера с заданного положения. Наша камера находится по центру экрана.
Все эти действия выполняются в CISGLRenderer.java. Для начала нужно изменить код вершинного шейдера, добавив туда ссылку на единственную матрицу uMVPMatrix.
Мы объединили обе матрицы в одну:
private final String vertexShaderCode =
“uniform mat4 uMVPMatrix; \n” +
“attribute vec4 vPosition; \n” +
“void main(){ \n” +
“ gl_Position = uMVPMatrix * vPosition; \n” +
“} \n”;
Здесь добавляется новая строка с матрицей, затем она умножается на vPosition для вычисления glPosition.
Также добавим несколько приватных членов матрицы для хранения разных данных:
private int mvpMatrixHandle; // matrix handle
private float[] uMVPMatrix = new float[16]; // объединенная матрица
private float[] vMatrix = new float[16]; // матрица обзора камеры
private float[] projMatrix = new float[16]; // проекционная матрица
Теперь допишем немного кода в onSurfaceCreated() и в onSurfaceChanged():
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
[ ... ]
mvpMatrixHandle = GLES20.
glGetUniformLocation(program, “uMVPMatrix”);
[ ... ]
}
public void onSurfaceChanged(GL10 gl, int width, int height)
{
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width/height;
Matrix.frustumM(projMatrix, 0, -ratio, ratio, -1, 1, 2, 7);
Matrix.setLookAtM(vMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f,0.0f);
}
Подключаемся к Матрице
Методы класса Matrix позволяют выполнять различные манипуляции с матрицами. frustumM() задает нашу проекционную матрицу в виде шести плоскостей отсечения. Плоскость отсечения в трехмерной графике говорит визуализатору, «на каком расстоянии» от наблюдателя он должен прекратить вычисления поверхностей. Это означает, что для самых далеких поверхностей вычисления не выполняются; таким образом, мы экономим вычислительные ресурсы.
Левая и правая плоскости отсечения задаются переменной ratio, это левый и правый края экрана. Нижняя и верхняя граница экрана задаются координатами -1 и 1. Два последних значения – «ближняя» и «дальняя» граница, у нас это 2 и 7 (они должны быть положительными). Мы получаем унифицированный дескриптор матрицы для последующего использования, а затем задаем матрицу обзора камеры методом setLookAtM. Она будет храниться в vMatrix со смещением 0. Точка наблюдения – (0, 0, -3), x = y = 0 и z = -3, т. е. точка наблюдения выходит «за» экран на три единицы системы координат.
- Метамодернизм в позднем творчестве В.Г. Сорокина
- ЛитРПГ - последняя отрыжка постмодерна
- "Ричард III и семиотика"
- 3D-визуализация обложки Ridero создаем обложку книги при работе над самиздатом.
- Архитектура метамодерна - говоря о современном искусстве, невозможно не поговорить об архитектуре. В данной статье будет отмечено несколько интересных принципов, характерных для построек "новой волны", столь притягательных и скандальных.
- Литература
- Метамодерн
- Рокер-Прометей против изначального зла в «Песне про советскую милицию» Вени Дркина, Автор: Нина Ищенко, к.ф.н, член Союза Писателей ЛНР - перепубликация из журнала "Топос".
- Как избавиться от комаров? Лучшие типы ловушек.
- Что делать если роблокс вылетает на windows
- Что делать, если ребенок смотрит порно?
- Почему собака прыгает на людей при встрече?
- Какое масло лить в Задний дифференциал (мост) Visco diff 38434AA050
- О чем может рассказать хвост вашей кошки?
- Верветки
- Отчетность бюджетных учреждений при закупках по Закону № 223-ФЗ
- Срок исковой давности как правильно рассчитать
- Дмитрий Патрушев минсельхоз будет ли преемником Путина
- Кто такой Владислав Поздняков? Что такое "Мужское Государство" и почему его признали экстремистским в России?
- Как правильно выбрать машинное масло в Димитровграде?
- Как стать богатым и знаменитым в России?
- Почему фильм "Пипец" (Kick-Ass) стал популярен по всему миру?
- Как стать мудрецом?
- Как правильно установить FreeBSD
- Как стать таким как Путин?
- Где лучше жить - в Димитровграде или в Ульяновске?
- Почему город Димитровград так называется?
- Что такое метамодерн?
- ВАЖНО! Временное ограничение движения автотранспортных средств в Димитровграде
- Тарифы на электроэнергию для майнеров предложено повысить
Центр области задан в (0f, 0f, 0f), т. е. совпадает с центром экрана, а восходящий вектор – в (0f, 1.0f, 0.0f). Восходящий вектор задает направление «вверх»; здесь это, как обычно, ось Y.
Наконец, изменим метод onDrawFrame() и воспользуемся всеми новыми матрицами:
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT |
GLES20.GL_DEPTH_BUFFER_BIT);
Matrix.multiplyMM(uMVPMatrix, 0, projMatrix, 0, vMatrix, 0);
GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, uMVPMatrix, 0);
GLES20.glUseProgram(program);
square.draw(positionHandle);
}
Метод multiplyMM умножает vMatrix на projMatrix и записывает результат в uMVPMatrix (все нули – смещения в результирующем массиве: у нас их нет). Метод glUniformMatrix4fv обновляет mvpMatrixHandle, который мы получили из вершинного шейдера в методе onSurfaceChanged() с помощью uMVPMatrix, не транспонируя никаких элементов (false).
Все готово! Перекомпилируйте программу, и вы должны увидеть настоящий квадрат.
Будьте осторожны! Любые ошибки в строках шейдеров OpenGL приведут к тому, что код перестанет работать, но компилятор Android их не увидит, так как для него это просто строки.
Если код работает не так, как ожидалось, внимательно проверьте заглавные буквы, пробелы и корректность используемых операторов в строках.
Добавим цвета
Сейчас мы задаем цвет квадрата вручную, как часть переменной fragmentShaderCode. Лучше передавать его извне. Измените переменные fragmentShaderCode и vertexShaderCode:
private final String vertexShaderCode =
“uniform mat4 uMVPMatrix; \n” +
“attribute vec4 vPosition; \n” +
“attribute vec4 aColour; \n” +
“varying vec4 vColour; \n” +
“void main(){ \n” +
“ vColour = aColour; \n” +
“ gl_Position = uMVPMatrix * vPosition; \n” +
“} \n”;
private final String fragmentShaderCode =
“precision mediump float; \n” +
“varying vec4 vColour; \n” +
“void main(){ \n” +
“ gl_FragColor = vColour; \n” +
“} \n”;
Цвет стал передаваться в aColour и vColour. Переменные с изменяемыми значениями служат в качестве интерфейса между вершинным и фрагментным шейдерами.
Цвет вершин задается в переменной aColour, которая не изменяется. Переменная vColour (которая изменяется) в точках вершин равна aColour, а затем во фрагментном шейдере интерполируется до цвета пикселей, который меняется между вершинами; так мы получаем разноцветную фигуру.
Затем в методе onSurfaceCreated() создадим объект Program, который использует эти значения:
private int colourHandle;
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
[ ... ]
program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glBindAttribLocation(program, 0, “vPosition”);
GLES20.glBindAttribLocation(program, 1, “aColour”);
GLES20.glLinkProgram(program);
positionHandle = GLES20.glGetAttribLocation(program, “vPosition”);
colourHandle = GLES20.glGetAttribLocation(program, “aColour”);
}
Данный код связывает дескриптор положения и дескриптор цвета с соответствующими переменными в вершинном шейдере (vPosition и aColour) и получает дескрипторы для обеих. Теперь измените вызов square.draw() в onDrawFrame(), передав в качестве аргументов дескрипторы положения и цвета.
В новой версии square.draw() будет всего две новых строки:
GLES20.glVertexAttribPointer(colourHandle, 4, GLES20.GL_ FLOAT, false, 0, squareColourBuffer);
GLES20.glEnableVertexAttribArray(colourHandle);
Обратите внимание, что мы здесь используем переменную squareColourBuffer, которая связана с colourHandle.
Понятное дело, именно в ней хранятся цвета вершин:
private FloatBuffer squareColourBuffer; float verticesColour[] =
1.0f, 0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f,
};
Для каждой вершины указан цвет в формате RGBA, и пиксели в этой вершине будут в него окрашены. По мере перемещения к следующей вершине визуализатор будет интерполировать цвета между цветами вершин, и один цвет будет плавно перетекать в другой (как на рисунке).
Если все строки (вершины) будут одного цвета в verticesColour, мы получим однотонный квадрат.
squareColourBuffer задается в конструкторе точно так же, как и squareBuffers. Скомпилируйте и запустите программу, и вы должны увидеть разноцветный квадрат.
Квадрат превращается в куб
Квадраты – это прекрасно, но истинная привлекательность OpenGL – в умении работать с трехмерными фигурами. Создадим класс Cube, чтобы нарисовать куб вместо нашего квадрата:
public class Cube {
private FloatBuffer cubeBuffer;
private FloatBuffer cubeColourBuffer;
private ShortBuffer cubeIndexBuffer;
float vertices[] = {
-0.5f, -0.5f, -0.5f,
// Координаты 8 вершин; подробности см. на DVD
};
float verticesColour[] = {
0.0f, 0.0f, 0.0f, 1.0f,
// Координаты 8 вершин; подробности см. на DVD
};
short[] cubeIndices = {
0, 4, 5,
// Всего 36 индексов в 12 группах по 3 (см. текст); подробности см. на DVD
};
public Cube() {
// Иициализируем cubeBuffer и cubeColourBuffer как делалось для Square.java
cubeIndexBuffer = ByteBuffer.allocateDirect(
cubeIndices.length * 4).
order(ByteOrder.nativeOrder()).asShortBuffer();
cubeIndexBuffer = cbb.asShortBuffer().put(cubeIndices).position(0);
}
public void draw(int positionHandle, int colourHandle) {
// Установим позицию и цвет VertexAttribPointers как в Square.java
GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, cubeIndices.length, GLES20.GL_UNSIGNED_SHORT, cubeIndexBuffer);
}
}
Главное отличие в том, что здесь используются координаты вершин куба. В массиве vertices определяется 8 вершин куба, которые можно пронумеровать от 0 до 7.
В массиве cubeIndices они группируются по три; каждая группа задает треугольник размером в половину грани куба. Это делается потому, что для рисования куба мы воспользуемся примитивом GL_TRIANGLE_STRIP (к сожалению, Android не поддерживает GL_QUADS, с которым мы могли бы определить грани куба, сгруппировав вершины по четыре).
Такое структурирование данных о вершинах/гранях и метод glDrawElements() позволяют задать куб с меньшим количеством вызовов.
Вызвав cube.draw() в CISGLRenderer.onDrawFrame(), вы все еще увидите квадрат – плоскую переднюю грань куба, на которую вы смотрите спереди. Чтобы увидеть трехмерность куба, его нужно немного повернуть.
Для этого воспользуемся еще одной матрицей mMatrix и добавим несколько строк в onSurfaceCreated() и одну строку в onDrawFrame():
private float mMatrix = new float[16];
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
[ ... rest of method as stands ... ]
Matrix.setIdentityM(mMatrix, 0);
Matrix.rotateM(mMatrix, 0, -40, 1, -1, 0);
}
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
Matrix.setIdentityM(uMVPMatrix, 0);
Matrix.multiplyMM(uMVPMatrix, 0, mMatrix, 0, uMVPMatrix, 0);
Matrix.multiplyMM(uMVPMatrix, 0, vMatrix, 0, uMVPMatrix, 0);
Matrix.multiplyMM(uMVPMatrix, 0, projMatrix, 0, uMVPMatrix, 0);
GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, uMVPMatrix, 0);
GLES20.glUseProgram(program);
cube.draw(positionHandle, colourHandle);
}
В методе onSurfaceCreated() мы инициализируем эту матрицу как единичную, затем поворачиваем ее на месте на -40 градусов вокруг оси XYZ (1, -1, 0) (попробуйте изменить эти числа и посмотрите, что происходит, если вы не знакомы с трехмерным поворотом).
Теперь в методе onDrawFrame() нужно умножить эту матрицу на матрицу обзора камеры (vMatrix) и на проекционную матрицу (projMatrix). Это означает, что нужно выделить строки и умножать каждую из них по очереди для получения объединенной матрицы, но по сути мы делаем здесь то же, что и раньше (объединяем различные матрицы в одну, чтобы применить ее к нашей фигуре), только с еще одной матрицей. Потом мы рисуем наш куб.
У нас должно получиться нечто похожее на куб, но его грани выглядят немного странно. Чтобы они смотрелись более солидно, добавьте еще две строки в метод onSurfaceCreated() после вызова glClearColor():
GLES20.glEnable(GLES20.GL_DEPTH_TEST);
GLES20.glDepthFunc(GLES20.GL_LEQUAL);
В первой строке включаются проверка глубины и буфер глубины. С включенным буфером глубины каждый раз при прорисовке пикселя его значение глубины сравнивается с сохраненным значением глубины.
Мы пользуемся параметром GL_LEQUAL, поэтому новый пиксель будет нарисован только в том случае, если он ближе к наблюдателю, чем старый, и, таким образом, виден ему.
Без этого, так как OpenGL рисует пиксели в любом порядке, можно получить причудливые результаты и частично видеть объекты «насквозь». Скомпилируйте и запустите программу и полюбуйтесь своим кубом! |
Куб в движении
Чтобы закончить нашу историю, заставим куб двигаться. Для этого нам снова достаточно лишь матрицы движения, но на сей раз в методе onDrawFrame():
Matrix.rotateM(mMatrix, 0, 1, 6, 2, 3);
Matrix.setIdentityM(uMVPMatrix, 0);
Каждый раз при перерисовке кадра (периодичность перерисовки будет зависеть от аппаратной начинки устройства и от того, что еще происходит в системе) куб будет поворачиваться на один градус вокруг оси (6, 2, 3). Опять же, попробуйте изменить эти числа, чтобы понять, что происходит (например, увеличьте «градусы», чтобы куб завертелся быстрее). Учтите, что порядок множителей при умножении матриц в onDrawFrame() имеет значение; попробуйте поменять местами строку multiplyMM mMatrix со строками vMatrix и projMatrix, и вы поймете, что я имею в виду!