Изучаем OpenGL ES2 под Андроид. Урок 1 — Самое начало

урок 1 основы openGL ES 2С исходными материалами можно ознакомиться на сайте http://www.learnopengles.com

Я привожу лишь его перевод и немного своего видения.

Итак.

Это у нас первый урок по OpenGL ES 2 для Андроида. В этом уроке, мы пройдемся по коду шаг за шагом и узнаем как создавать среду для работы OpenGL ES 2 и выводить изображение на экран. Мы также узнаем, что такое шейдеры (shaders) и как они работают, а также как матрицы используются для преобразования сцены в изображение, которое вы видите на экране телефона. Ну и в завершение, мы узнаем то, что необходимо будет добавить в манифест, чтобы наш Android знал, что вы планируете использовать OpenGL ES 2.

Подготовка

Прежде чем мы начнем, вам нужно убедиться, что у вас на компьютере установлены все необходимые инструменты для работы:

К сожалению, стандартный эмулятор Android не поддерживает OpenGL ES 2, так что вам понадобится реальное устройство или эмулятор от GENYMOTION для запуска приложений описанных в наших уроках. На сегодняшний день практически все устройства поддерживают OpenGL ES 2, так что все, что вам нужно сделать, это включить режим разработчика на телефоне и подключить его к вашему ПК. Так как OpenGL ES 1.хх уже морально устарел, лучше сразу осваивать ES 2. Он проще и намного богаче в своих возможнастях. Но все по порядку.

Базовые знания

Читатель должен быть знаком с Android и Java на базовом уровне. Уроки по Android будут весьма полезными для новичков.

Приступаем к работе

Мы пройдемся по всему приведенному ниже коду с объяснениями, выясним что делает каждая его часть. Вы можете скопировать код частями при создании собственного проекта или загрузить готовый проект по ссылке в конце урока. Если у вас все установлено и готово к работе, тогда вперед.. Создаем новый проект Android в Eclipse или в чем вы там работаете. Названия проектов не имеют значения, но урок, который на который я буду ссылаться, имеет имя LessonOneActivity.

Давайте посмотрим на код:

/** Создаем экземпляр нашего GLSurfaceView */
private GLSurfaceView mGLSurfaceView;

GLSurfaceView это специальная Вьюшка (поверхность, область для отображения изображения), которая управляет поверхностью OpenGL и преобразует ее в понятную Андроиду для отображения среду. Она также имеет множество функций, которые делают OpenGL проще в использовании. Вот некоторые из функций, самые распространенные:

  • GLSurfaceView обеспечивает работу в отдельном потоке (thread) для визуализации OpenGL, таким образом основной поток у нас не занят.
  • поддерживает непрерывный или по требованию рендеринг (обновление, перерисовка содержимого на экране) .
  • поддерживает настройки экрана для EGL, интерфейса между OpenGL и нашей оконной системой.

GLSurfaceView осуществляет настройку и использование OpenGL для Android относительно безболезненно.

@Override
public void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);

    mGLSurfaceView = new GLSurfaceView(this);

    // Проверяем поддереживается ли OpenGL ES 2.0.
    final ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
    final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
    final boolean supportsEs2 = configurationInfo.reqGlEsVersion >= 0x20000;

    if (supportsEs2)
    {
        // Запрос OpenGL ES 2.0 для установки контекста.
        mGLSurfaceView.setEGLContextClientVersion(2);

        // Устанавливаем рендеринг, создаем экземпляр класса, он будет описан ниже.
        mGLSurfaceView.setRenderer(new LessonOneRenderer());
    }
    else
    {
        // Устройство поддерживает только OpenGL ES 1.x 
        // опишите реализацию рендеринга здесь, для поддержку двух систем ES 1 and ES 2.
        return;
    }

    setContentView(mGLSurfaceView);
}

 

Метод OnCreate() нашей главной Активити является важной составляющей для создания отображения содержимого нашего OpenGL. В методе OnCreate(), первое, что мы делаем после вызова суперкласса, создает экземпляр класса нашего GLSurfaceView. Затем мы должны выяснить, поддерживает ли наша система OpenGL ES 2. Чтобы сделать это, мы получаем из ActivityManager информацию, которая позволяет нам вытянуть из состояния системы конфигурацию устройства. А уже из нее мы получаем информацию, которая поможет нам понять поддерживает ли наше устройство OpenGL ES 2.

Как только мы узнаем, что устройство поддерживает OpenGL ES 2, мы создаем поверхность для OpenGL ES 2, а затем переходим к реализации нашей визуализации. Этот блок будет выполняться системой каждый раз, когда необходимо будет создать или перерисовать новый кадр. Мы можем также реализовать поддержку и OpenGL ES 1.x, написав для этого другой класс для нашей визуализации, следуя требованиям API. Но для нашего урока мы будем реализовывать только поддержку OpenGL ES 2.

Наконец, мы устанавливаем нашу поверхность как GLSurfaceView, которая  сообщит Android что его содержимое экрана должно быть заполнено созданной поверхностью OpenGL. Таким образом мы отображаем OpenGL. Все просто!

@Override
protected void onResume()
{
    // Вызывается GL surface view's onResume() когда активити переходит onResume().
    super.onResume();
    mGLSurfaceView.onResume();
}

@Override
protected void onPause()
{
    // Вызывается GL surface view's onPause() когда наше активити onPause().
    super.onPause();
    mGLSurfaceView.onPause();
}

GLSurfaceView требует, чтобы мы вызывали его методы onResume() и OnPause(), когда родительское Активити вызывает свои методы onResume() и onPaused(). Мы добавляем вызов этих методов, чтобы правильно возобновлять и завершать нашу деятельность.

Визуализация 3D мира

В этом разделе мы изучим, как OpenGL ES 2 работает и как происходит отрисовка моделей на экране. В главной Активити мы вызвали пользовательский интерфейс GLSurfaceView.Renderer относящийся к GLSurfaceView. Он имеет три важных методах, которые будут автоматически вызывается системой каждый раз, когда происходят такие события:

public void onSurfaceCreated(GL10 glUnused, EGLConfig config)этот метод вызывается, системой когда поверхность создается впервые. Он также будет вызываться, если мы теряем содержимое нашей поверхность в результате пересоздания ее самой системой.

public void onSurfaceChanged(GL10 glUnused, int width, int height)этот метод вызывается всякий раз, когда происходит изменения поверхности, например, при переключении с портретной на альбомную ориентацию. Он также вызывается после того как поверхность была создана.

public void onDrawFrame(GL10 glUnused)этот метод вызывается всякий раз, когда необходимо отрисовать новый кадр. Режим перерисовки можно задать как по команде, так и непрерывно.

Вы, возможно, заметили, что мы используем параметр GL10 glUnused для получения доступа. Но на самом деле мы не используем GL10, когда рисуем с помощью OpenGL ES 2, вместо этого, мы используем статические методы класса GLES20. GL10 параметр используется лишь потому, что имеет идинтичный интерфейс, что и для OpenGL ES 1.x,

Прежде, чем что-то отобразить, нам нужно иметь что-то, что мы могли бы увидеть. В OpenGL ES 2, мы передаем структуру материал и формы объекта для отображения, используя массивов чисел. Эти величины могут использоваться для представления положения фигур, цвета их поверхности, или для тех параметров, которые нам необходимы. В нашем примере мы отобразим три треугольника.

// Новые члены класса
/** Будем хранить наши данные в числовом буфере. */
private final FloatBuffer mTriangle1Vertices;
private final FloatBuffer mTriangle2Vertices;
private final FloatBuffer mTriangle3Vertices;

/** Количество байт занимаемых одним числом. */
private final int mBytesPerFloat = 4;

/**
 * Инициализируем модель с данными.
 */
public LessonOneRenderer()
{
    // Треугольники красный, зеленый, и синий.
    final float[] triangle1VerticesData = {
            // X, Y, Z,
            // R, G, B, A
            -0.5f, -0.25f, 0.0f,
            1.0f, 0.0f, 0.0f, 1.0f,

            0.5f, -0.25f, 0.0f,
            0.0f, 0.0f, 1.0f, 1.0f,

            0.0f, 0.559016994f, 0.0f,
            0.0f, 1.0f, 0.0f, 1.0f};

    ...

    // Инициализируем буфер.
    mTriangle1Vertices = ByteBuffer.allocateDirect(triangle1VerticesData.length * mBytesPerFloat)
    .order(ByteOrder.nativeOrder()).asFloatBuffer();

    ...

    mTriangle1Vertices.put(triangle1VerticesData).position(0);

    ...
}

Итак, что же все это значит? Если вы когда-либо работали с OpenGL 1, вы могли бы использовать такой код:

glBegin (GL_TRIANGLES);
glVertex3f (-0.5f,-0.25f, 0.0f);
glColor3f (1.0f, 0.0f, 0.0f);
...
glEnd ();

В OpenGL ES 2 это уже не работает. Все намного проще! Вместо определения параметров точки через вызовы кучи методов, мы определяем все с помощью числового массива. Давайте взглянем на наш массив еще раз:

final float[] triangle1VerticesData = {
                // X, Y, Z,
                // R, G, B, A
                -0.5f, -0.25f, 0.0f,
                1.0f, 0.0f, 0.0f, 1.0f,
                ...

Так задается одна точка треугольника. Мы настраиваем систему так, чтобы первые три цифры соответствовали позициям (X, Y, Z), и последние четыре цифры определяли цвет (красный R, зеленый G, синий B, и альфа A (прозрачность)). Вам не нужно беспокоиться о том, как этот массив читается, просто помните, что когда мы хотим передать объекты в OpenGL ES 2, мы должны передать все в виде массива данных за один раз.

Подготовка буфера данных
// Инициализация буфера.
        mTriangle1Vertices = ByteBuffer.allocateDirect(triangle1VerticesData.length * mBytesPerFloat)
        .order(ByteOrder.nativeOrder()).asFloatBuffer();
        ...
        mTriangle1Vertices.put(triangle1VerticesData).position(0);

Мы пишем весь код в Java под Android, но родная реализация OpenGL ES2 на самом деле написана и выполняется на Cи. Прежде чем перейти к нашим данным OpenGL, мы должны преобразовать их в понятный для него вид. Java и нативная система(native system) не могут хранить байты в одинаковом порядке, поэтому мы используем специальный набор классов буфера и создаем ByteBuffer, достаточно большой для хранения всех наших данных. Далее мы используем его для хранения наших байтов в собственном порядке. После этого мы преобразовываем его в FloatBuffer, и теперь мы можем использовать его для хранения данных в виде чисел с плавающей запятой. Наконец, мы копируем наш массив в буфер и перемещаемся на позицию 0 в этом массиве.

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

Применение матрицы ВИДА
// Определение новых классов
 /**
 * Определяем матрицу ВИДА. Её можно рассматривать как камеру. Эта матрица описывает пространство;
 * она задает положение предметов относительно нашего глаза.
 */
private float[] mViewMatrix = new float[16];

@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
{
    // Устанавливаем цвет фона светло серый.
    GLES20.glClearColor(0.5f, 0.5f, 0.5f, 0.5f);

    // Положение глаза, точки наблюдения в пространстве.
    final float eyeX = 0.0f;
    final float eyeY = 0.0f;
    final float eyeZ = 1.5f;

    // На какое расстояние мы можем видеть вперед. Ограничивающая плоскость обзора.
    final float lookX = 0.0f;
    final float lookY = 0.0f;
    final float lookZ = -5.0f;

    // Устанавливаем вектор. Положение где наша голова находилась бы если бы мы держали камеру.
    final float upX = 0.0f;
    final float upY = 1.0f;
    final float upZ = 0.0f;

    // Устанавливаем матрицу ВИДА. Она описывает положение камеры.
    // Примечание: В OpenGL 1, матрица ModelView используется как комбинация матрицы МОДЕЛИ
    // и матрицы ВИДА. В OpenGL 2, мы можем работать с этими матрицами отдельно по выбору.
    Matrix.setLookAtM(mViewMatrix, 0, eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ);

    ...

Другая «забавная» тема — это тема матриц! Они со временем станут вашими лучшими друзьями, когда вы начнете осваивать 3D-программирование. Так что, следует их хорошо изучить.

Когда наша поверхность создается, первое, что мы делаем — устанавливаем ей светло серый цвет фона. Значение Альфа(прозрачности) цвета также было установлено, но мы не применяем альфа-смешивания в этом уроке, так что по сути мы это значение не используем. Нам нужно установить цвет лишь один раз, менять его мы пока не планируем.

Второе, что мы делаем это устанавливаем нашу матрицу ВИДА. Есть несколько видов матриц которые мы используем, и все они делают что-то важное:

  1. Матрица МОДЕЛИ. Эта матрица используется для размещения нашей модели где-то в «пространстве». Например, если у вас есть модель автомобиля, и вы хотите расположить ее на 1000 метров на восток, вы будете использовать для этого матрицу МОДЕЛИ.
  2. Матрица ВИДА. Эта матрица представляет собой камеру(глаз). Если мы хотим посмотреть на наш автомобиль, который расположен на 1000 метров на восток, нам придется самим переместиться на 1000 метров на восток (если подумать иначе, что мы не подвижны, тогда весь мир будет перемещаться относительно нас на 1000 метров, но уже на запад). Мы используем для этого матрицу ВИДА.
  3. Матрица ПРОЕКЦИИ. Так как наши экраны на самом деле плоские, нам необходимо проделать некую трансформацию «проекта», чтобы получить видимое изображение на плоской поверхности экрана наших объемных моделей с учетом 3D перспективы. Для этого и используется матрица ПРОЕКЦИИ.

Хорошее объяснение этому можно найти в книге Beginning Android Games / Программирование игр под Android Автор: Mario ZechnerЯ рекомендую прочитать его на досуге!

В OpenGL 1, матрицы МОДЕЛИ и ПРОЕКЦИИ объединены и камера располагается в точке (0, 0, 0) и по ходу движения в Z направлении.

Нам не нужно придумывать велосипед, создавать все матрицы вручную. Для этого в арсенале у Android есть класс помощник Matrix, который делает всю тяжелую работу за нас. В данном примере создана матрицы ВИДА для камеры, которая расположена позади основного экрана и смотрит прямо на противоположную плоскость.

Определение вершинных и фрагментных шейдеров
final String vertexShader =
    "uniform mat4 u_MVPMatrix;      \n"     // Константа отвечающая за комбинацию матриц МОДЕЛЬ/ВИД/ПРОЕКЦИЯ.

  + "attribute vec4 a_Position;     \n"     // Информация о положении вершин.
  + "attribute vec4 a_Color;        \n"     // Информация о цвете вершин.

  + "varying vec4 v_Color;          \n"     // Это будет передано в фрагментный шейдер.

  + "void main()                    \n"     // Начало программы вершинного шейдера.
  + "{                              \n"
  + "   v_Color = a_Color;          \n"     // Передаем цвет для фрагментного шейдера.
                                            // Он будет интерполирован для всего треугольника.
  + "   gl_Position = u_MVPMatrix   \n"     // gl_Position специальные переменные используемые для хранения конечного положения.
  + "               * a_Position;   \n"     // Умножаем вершины на матрицу для получения конечного положения 
  + "}                              \n";    // в нормированных координатах экрана.

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

Каждый шейдер в основном состоит из точки входа (начала программы), выхода, и самой логики программы. Сначала определим константу(uniform) u_MVPMatrix, она представляет собой комбинированную матрицу, содержащую значения всех наших преобразований. Эта матрица используется для всех вершин при проецировании их на экран. Потом мы определяем две константы атрибуты — положение вершин и цвет. Значения в них будут переданы из числового байт буфера, который мы описали ранее. В нем мы уже содержим данные о положении и цвете для каждой нашей вершины. Далее определяем переменные, значения которых описывают наши треугольники, а далее мы передаем их значения в фрагментный шейдер. В шейдере над ними проводятся необходимые операции, вычисления, которые касаются каждого пикселя. 

Допустим, мы определили, что у треугольника вершины должны быть красного, зеленого и синего цвета. А размером он должен быть таким, чтобы занять 10 пикселей на экране. Когда программа фрагментного шейдера будет запущена, в нем уже будут значения переменных отвечающих за цвет для каждого пикселя. В одной вершине значение будет соответствовать красному цвету, но посредине между красной и синей вершиной значение будет интерполироваться(усредняться) что выразиться в виде пурпурного цвета.

Кроме установки цвета, мы также сообщили OpenGL окончательное положение вершин расположенных на экране. Далее приведен пример программы фрагментного шейдера:

final String fragmentShader =
    "precision mediump float;       \n"     // Устанавливаем по умолчанию среднюю точность для переменных. Максимальная точность
                                            // в фрагментном шейдере не нужна.
  + "varying vec4 v_Color;          \n"     // Цвет вершинного шейдера преобразованного
                                            // для фрагмента треугольников.
  + "void main()                    \n"     // Точка входа для фрагментного шейдера.
  + "{                              \n"
  + "   gl_FragColor = v_Color;     \n"     // Передаем значения цветов.
  + "}                              \n";

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

Более подробную информацию можно найти в OpenGL ES 2 Краткий справочник (правда на Английском).

Загрузка программы шейдеров в OpenGL
// Загрузка вершинного шейдера.
int vertexShaderHandle = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);

if (vertexShaderHandle != 0)
{
    // Передаем в наш шейдер программу.
    GLES20.glShaderSource(vertexShaderHandle, vertexShader);

    // Компиляция шейреда
    GLES20.glCompileShader(vertexShaderHandle);

    // Получаем результат процесса компиляции
    final int[] compileStatus = new int[1];
    GLES20.glGetShaderiv(vertexShaderHandle, GLES20.GL_COMPILE_STATUS, compileStatus, 0);

    // Если компиляция не удалась, удаляем шейдер.
    if (compileStatus[0] == 0)
    {
        GLES20.glDeleteShader(vertexShaderHandle);
        vertexShaderHandle = 0;
    }
}

if (vertexShaderHandle == 0)
{
    throw new RuntimeException("Error creating vertex shader.");
}

Во-первых, мы предаем наши программы. Если это нам удалось, то мы получим ссылку на результирующий объект. Затем мы используем эту ссылку, чтобы проверить прошел ли шейдер компиляцию. Получаем статус compileStatus от OpenGL и проверяем на успешность компиляции. Если были ошибки, мы можем использовать GLES20.glGetShaderInfoLog (shader), чтобы выяснить причину. Выполняем те же шаги, чтобы загрузить фрагментный шейдер.

Объединение вершинного и фрагментного шейдеров
/ Создаем объект программы вместе со ссылкой на нее.
int programHandle = GLES20.glCreateProgram();

if (programHandle != 0)
{
    // Подключаем вершинный шейдер к программе.
    GLES20.glAttachShader(programHandle, vertexShaderHandle);

    // Подключаем фрагментный шейдер к программе.
    GLES20.glAttachShader(programHandle, fragmentShaderHandle);

    // Подключаем атрибуты цвета и положения
    GLES20.glBindAttribLocation(programHandle, 0, "a_Position");
    GLES20.glBindAttribLocation(programHandle, 1, "a_Color");

    // Объединяем оба шейдера в программе.
    GLES20.glLinkProgram(programHandle);

    // Получаем ссылку на программу.
    final int[] linkStatus = new int[1];
    GLES20.glGetProgramiv(programHandle, GLES20.GL_LINK_STATUS, linkStatus, 0);

    // Если ссылку не удалось получить, удаляем программу.
    if (linkStatus[0] == 0)
    {
        GLES20.glDeleteProgram(programHandle);
        programHandle = 0;
    }
}

if (programHandle == 0)
{
    throw new RuntimeException("Error creating program.");
}

Прежде чем мы сможем использовать наш вершинный и фрагментный шейдер, мы должны объединить их вместе в одну программу. При этом мы соединяем выход программы вершинного шейдера с входом программы фрагментного шейдера. Они выполняются как бы по цепочке. Это  нам позволяет использовать начало нашей шейдерной программы для отрисовки наших моделей.

Мы создаем новый объект программы OpenGL, и если это нам удается, мы затем в него передаем наши шейдеры. Также передаем, как атрибуты, положение и цвет. Все, что нам осталось — это присоединить атрибуты к программе и объединить шейдеры.

//Новые члены класса
/** Используется для передачи в матрицу преобразований. */
private int mMVPMatrixHandle;

/** Используется для передачи информации о положении модели. */
private int mPositionHandle;

/** Используется для передачи информации о цвете модели. */
private int mColorHandle;

@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
{
    ...

    // Установить настройки вручную. Это будет позже использовано для передачи значений в программу.
    mMVPMatrixHandle = GLES20.glGetUniformLocation(programHandle, "u_MVPMatrix");
    mPositionHandle = GLES20.glGetAttribLocation(programHandle, "a_Position");
    mColorHandle = GLES20.glGetAttribLocation(programHandle, "a_Color");

    // Сообщить OpenGL чтобы использовал эту программу при рендеринге.
    GLES20.glUseProgram(programHandle);
}

После того, как мы успешно объединили все элементы в нашей программе, мы закончили большую часть тех задач, которые нам необходимы для работы. Первая задача заключалась получения ссылки для передачи данных в программу. Потом мы сообщаем OpenGL использовать эту программу для рисования. Так как мы запускаем программу только раз, то мы можем реализовать все в методе onSurfaceCreated() вместо onDrawFrame().

Установка проекции перспективы
// Новые члены класса
/** Сохраняем матрицу проекции.Она используется для преобразования трехмерной сцены в 2D изображение. */
private float[] mProjectionMatrix = new float[16];

@Override
public void onSurfaceChanged(GL10 glUnused, int width, int height)
{
    // Устанавливаем OpenGL окно просмотра того же размера что и поверхность экрана.
    GLES20.glViewport(0, 0, width, height);

    // Создаем новую матрицу проекции. Высота остается та же,
    // а ширина будет изменяться в соответствии с соотношением сторон.
    final float ratio = (float) width / height;
    final float left = -ratio;
    final float right = ratio;
    final float bottom = -1.0f;
    final float top = 1.0f;
    final float near = 1.0f;
    final float far = 10.0f;

    Matrix.frustumM(mProjectionMatrix, 0, left, right, bottom, top, near, far);
}

Наш метод onSurfaceChanged() вызывается не реже одного раза, а также каждый раз, когда происходят изменения поверхности. Нам нужно лишь пересоздать нашу матрицу проекции каждый раз, когда на экране что-то изменилось. Метод onSurfaceChanged() идеально для этого подходит.

Рисуем на экране!
// Новые члены класса
    /**
     * Сохраняем матрицу моделей. Она используется для перемещения моделей со своим пространством  (где каждая модель рассматривается
     * относительно центра системы координат нашего мира) в пространстве мира.
     */
    private float[] mModelMatrix = new float[16];

    @Override
    public void onDrawFrame(GL10 glUnused)
    {
        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);

        // Делаем полный оборот при вращении за 10 секунд.
        long time = SystemClock.uptimeMillis() % 10000L;
        float angleInDegrees = (360.0f / 10000.0f) * ((int) time);

        // Рисуем треугольники плоскостью к нам.
        Matrix.setIdentityM(mModelMatrix, 0);
        Matrix.rotateM(mModelMatrix, 0, angleInDegrees, 0.0f, 0.0f, 1.0f);
        drawTriangle(mTriangle1Vertices);

        ...
}

В этом методе все наши модели будут отрисовываться на экране. Вначале мы очищаем экран, чтобы не получить непредвиденных цветовых эффектов на наших треугольниках. Далее мы начинаем их плавно вращать, используя для задания угла поворота текущее время. Каждый раз, когда вы хотите что-то анимировать на экране, лучше всего использовать время для задачи движения вместо фиксированной частоты кадров, т.к. на различных устройствах может отличаться скорость анимации при задании фиксированной частоты кадров, в зависимости от производительности процессора.

Фактически изображение создается в методе drawTriangle:

// Новые члены класса
/** Выделяем массив для хранения объединеной матрицы. Она будет передана в программу шейдера. */
private float[] mMVPMatrix = new float[16];

/** Количество элементов в вершине. */
private final int mStrideBytes = 7 * mBytesPerFloat;

/** Смещение в массиве данных. */
private final int mPositionOffset = 0;

/** Размер массива позиций в элементах. */
private final int mPositionDataSize = 3;

/** Смещение для данных цвета. */
private final int mColorOffset = 3;

/** Размер данных цвета в элементах. */
private final int mColorDataSize = 4;

/**
 * Рисуем треугольники из массива данных вершин.
 *
 * @param aTriangleBuffer Буфер содержащий данные о вершинах.
 */
private void drawTriangle(final FloatBuffer aTriangleBuffer)
{
    // Передаем значения о расположении.
    aTriangleBuffer.position(mPositionOffset);
    GLES20.glVertexAttribPointer(mPositionHandle, mPositionDataSize, GLES20.GL_FLOAT, false,
            mStrideBytes, aTriangleBuffer);

    GLES20.glEnableVertexAttribArray(mPositionHandle);

    // Передаем значения о цвете.
    aTriangleBuffer.position(mColorOffset);
    GLES20.glVertexAttribPointer(mColorHandle, mColorDataSize, GLES20.GL_FLOAT, false,
            mStrideBytes, aTriangleBuffer);

    GLES20.glEnableVertexAttribArray(mColorHandle);

    // Перемножаем матрицу ВИДА на матрицу МОДЕЛИ, и сохраняем результат в матрицу MVP
    // (которая теперь содержит модель*вид).
    Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);

    // Перемножаем матрицу модели*вида на матрицу проекции, сохраняем в MVP матрицу.
    // (которая теперь содержит модель*вид*проекцию).
    Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0);

    GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
}

Вы помните, эти числовые буферы мы определили, когда первоначально создавали рендеринг? Теперь мы будем использовать все вместе. Нам нужно сообщить OpenGL, чтобы он обрабатывал данные с помощью GLES20.glVertexAttribPointer(). Давайте посмотрим на первый вызов.

// Передаем значения о расположении.
aTriangleBuffer.position(mPositionOffset);
GLES20.glVertexAttribPointer(mPositionHandle, mPositionDataSize, GLES20.GL_FLOAT, false,
        mStrideBytes, aTriangleBuffer);
GLES20.glEnableVertexAttribArray(mPositionHandle);

Мы устанавливаем в нашем числовом буфере смещение в элементах относительно начального элемента буфера. Мы сообщаем OpenGL чтобы он использовал эти данные и передал их в вершинный шейдер и применил все к нашим  атрибутам. Мы также должны сообщить OpenGL, количество элементов(шаг) между значениями описывающими каждую вершину.

Примечание: шаг должен быть определен в байтах . Пусть у нас есть 7 элементов (3 описывают положение вершины и 4 ее цвет) между вершинами, у нас фактически занято 28 байт, так как каждое число с плавающей точкой занимает 4 байта. Если мы забудем про этот шаг у нас в процессе выполнения программы не будет никаких ошибок, но вы будете удивлены, почему на экране будет пусто!

Наконец, мы отключаем вершинные атрибуты и переходим к следующему атрибуту. Немного ниже мы создадим комбинированную матрицу проецирующую точки на экран. Мы могли бы сделать это и в вершинном шейдере, но так как мы хотим сделать все правильно, то такой подход позволит нам кешировать результат. Далее помещаем в конечную матрицу вершинных шейдеров с помощью команд GLES20.glUniformMatrix4fv() и GLES20.glDrawArrays(). Происходит преобразование наших точек в треугольники и отображение их на экране.

Подводим итоги

Вот и все! Это был большой урок, и очень хорошо, если вы довели все до конца. Мы узнали, как создавать поверхность с помощью OpenGL, как создавать модели из данных и загружать их в вершинный и фрагментный шейдеры, как создавать матрицу преобразований, и, наконец, как объединить все это в одно целое. Если все отработало хорошо, вы должны увидеть что-то похожее на рисунке справа. результат труда

Этот урок был объемным и чтобы переварить его вам, возможно, придется перечитать все еще несколько раз, чтобы понять насколько это все просто. OpenGL ES 2 требует немного больше времени в своей настройке, перед запуском, но как только вы пройдете весь цикл несколько раз, вы сможете делать это с закрытыми глазами.

Публикация на Google Play Market

При разработке приложений, необходимо чтобы большее количество пользователей могло установить наше приложение. Для этого нет лучше места чем магазин Google Play для наших Андроид приложений. Чтобы избавиться от плохих отзывов и низкого рейтинга в результате падения нашего приложения на устройствах пользователей, мы должны указать на каких устройствах разрешено устанавливать данное приложение. Чтобы отсеять телефоны не поддерживающие OpenGL ES2 нам необходимо добавить следующие строчки в файл manifest:

<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />

Эти строчки сообщают устройству что ваше приложение требует поддержки OpenGL ES 2, таким образом будут отсеяны все пользователи, чьи устройства не подходят под этот критерий.

Для самостоятельной работы

Попробуйте изменить скорость анимации, положение вершин или их цвет, и посмотрите, что получается!

Полный исходный код для этого урока можно загрузить с сайта проекта на GitHub.

Установочный файл приложения с этого урока можете скачать непосредственно с сайта Google Play.

Я также рекомендую посмотреть исходный код в приложении ApiDemos, который вы можете найти в папке Samples в папке Android SDK.

Comments are closed.