Изучаем OpenGL ES2 под Андроид. Урок 4 — Введение в основы текстурирования.

Введение в текстурирование OpenGL ES

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

Предположения и предпосылки

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

Основы текстурирования

Искусство наложения текстур (наряду с освещением) является одной из самых важных частей создания реалистично выглядящих 3D миров. Без наложения текстур, все предметы будут с гладкой поверхностью и будут казаться искуственными, как игры с приставок 90-х годов.

Первые игры, начавшие по максимуму использовать текстуры были Doom и Duke Nukem 3D, они смогли значительно повысить реалистичность игрового процесса через дополнительное визуальное воздействие — это были игры, которые могли бы по-настоящему напугать нас, если играть в них ночью и в темноте .

Вот посмотрите на сцену без текстур и с текстурами:

На фрагменте освещения; центре между четырьмя вершинами квадрата.На фрагменте освещения с текстурирования: в центре между четырьмя вершинами квадрата.

 

 

 

 

 

 

 

 

 

 

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

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

Текстурные координаты

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

В большинстве компьютерных координатных систем отображения, ось Y направлена ​​вниз . Это означает, что верхний левый угол соответствует изображению в точке (0, 0), а его значение увеличивается при движении в направлении оси вниз. Другими словами, ось Y перевернута в системе координат OpenGL и отличается от большинства систем координат работающих с изображением. Этот момент нужно учитывать.

Координат текстуры OpenGL система.

Координаты текстур в системе OpenGL

Основы наложения текстур

В этом уроке мы будем изучим базовые 2D текстуры (GL_TEXTURE_2D) с параметрами красного, зеленого и синего цветов (GL_RGB). OpenGL ES также поддерживает другие виды текстур, которые позволят вам создать другие, более специализированные эффекты. Мы будем использовать для обработки текстур фильтры GL_NEAREST. GL_LINEAR и MIP-изображения, которые мы рассмотрим в следующем уроке.

Давайте возьмем пример кода и посмотрим, как использовать основные приемы текстурирования в Android!

Вершинный шейдер

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

attribute vec2 a_TexCoordinate; // Сюда передается информация о вершинах текстуры.

...

varying vec2 v_TexCoordinate;   // Это будет передано во фрагментый шейдер.

...
// Передаем координаты текстур.
v_TexCoordinate = a_TexCoordinate;

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

Фрагментный шейдер
uniform sampler2D u_Texture;    // Наша Текстура.

...

varying vec2 v_TexCoordinate; // Интерполированные координаты текстуры из предыдущего шейдера.

...

// Добавляем затухание.
 diffuse = diffuse * (1.0 / (1.0 + (0.10 * distance)));

...

// Добавляем окружающее освещение
 diffuse = diffuse + 0.3;

...

// Перемножаем цвет и коэффициент затухания для расчета окончательного значения цвета.

gl_FragColor = (v_Color * diffuse * texture2D(u_Texture, v_TexCoordinate));

Добавим новую униформу типа sampler2D В неё мы передадим всю информацию о текстуре (а не только одни координаты). Переменная передается в интерполированные текстурные координаты из вершинного шейдера, далее мы вызываем метод  texture2D(texture, textureCoordinate) чтобы прочитать значения текстуры в заданной координате. Далее мы берем это значение и умножаем его на другие параметры, чтобы получить окончательное цветное изображение.

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

Чтение текстуры из графического файла
public static int loadTexture(final Context context, final int resourceId)
{
    final int[] textureHandle = new int[1];

    GLES20.glGenTextures(1, textureHandle, 0);

    if (textureHandle[0] != 0)
    {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inScaled = false;   // No pre-scaling

        // Читаем из ресурсов
        final Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceId, options);

        // Привязываем к текстуре в OpenGL
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0]);

        // Устанавливаем фильтры
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);

        // Загружаем изображение к прикрепленной текстуре.
        GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);

        // Освобождаем изображение, т.к. данные уже были переданы в OpenGL.
        bitmap.recycle();
    }

    if (textureHandle[0] == 0)
    {
        throw new RuntimeException("Error loading texture.");
    }

    return textureHandle[0];
}

Наш код выполняет чтение графического файла из папки res в Android приложении и загружает его в OpenGL. Я поясню, что делает каждая часть.

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

final int[] textureHandle = new int[1];
GLES20.glGenTextures(1, textureHandle, 0);

OpenGL позволяет создавать несколько ссылок на текстуры, но мы создаем только одну.

Как только мы получим ссылку на текстуру, мы используем ее для дальнейшей загрузки. Во-первых, мы должны получить текстуру в формате, понятный OpenGL. Мы не можем просто передать в него данные из PNG или JPG, потому что он не поймет их. Первый шаг, который мы должны сделать — это преобразовать графический файл в Bitmap объект:

final BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;   // Без предварительного маштабирования 
// Читаем из папки ресурсов
final Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceId, options);

По умолчанию, Android использует масштабирование графических изображений в соответствии с разрешением экрана Вашего устройства, вне зависимости от исходного размера изображения. Поэтому мы сообщаем системе Аndroid чтобы он не проводил эту операцию inScaled=false .

// Привязываем текстуру в ОpenGL
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0]);

// Устанавливаем фильты
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);

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

  • GL_TEXTURE_MIN_FILTER - сообщает OpenGL, какой тип фильтрации применять при отображении текстуры размером меньшим, чем его исходный размер в пикселях.
  • GL_TEXTURE_MAG_FILTER - сообщает OpenGL, какой тип фильтрации применять при увеличении текстуры относительно своего первоначального размера в пикселях.
// Загрузка графического объекта в привязанную текстуру.
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);

// Освобождаем объект, т.к. данные уже загружены в OpenGL.
bitmap.recycle();

У Androidа есть очень полезная утилита для загрузки графических изображений непосредственно в OpenGL. После передачи изображения в объект, GLUtils.texImage2D () позаботится об остальном. Вот конструктор этого метода:

public static void texImage2D (int target, int level, Bitmap bitmap, int border)

Нам необходима базовая 2D текстура, поэтому мы передаем GL_TEXTURE_2D в качестве первого параметра. Второй параметр  MIP-маппинг, позволяющих подготовить изображение для разных уровней маштабирования. Мы не используем множественное отображение, поэтому передаем 0, уровень устанавливается по умолчанию. Далее передаем наш графический объект, без использования границы, поэтому последний параметр 0.

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

Применение текстуры к нашей сцене

Во-первых, нам нужно добавить различные члены класс для хранения вещей, что нужно для нашей текстуры:

 

 

 

Comments are closed.