суббота, 3 сентября 2016 г.

­Изучаем OpenGL ES2 для Android

Урок №4. Текстуры


Перед тем как начать
   Если вы новичок в OpenGL ES, рекомендую сначала изучить предыдущие 3 урока.

  Основы кода, используемого в этой статье, взяты отсюда:

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


Использование текстур также позволяет воспроизвести малые детали поверхности, создание которых полигонами оказалось бы чрезмерно ресурсоёмким. Например, шрамы на коже, складки на одежде, мелкие камни и прочие предметы на поверхности стен и почвы.
Качество текстурированной поверхности определяется текселями — количеством пикселей на минимальную единицу текстуры. Так как сама по себе текстура является изображением, разрешение текстуры и её формат играют большую роль, которая впоследствии сказывается на быстродействии и  качестве графики в приложении.
 Текстурные координаты
В OpenGL координаты текстур задают обычно в координатах (s, t) или (u,v) вместо (х, у). (s, t) представляет собой тексель текстуры, которая затем преобразуется в многоугольник.
В большинстве компьютерных координатных систем отображения, ось Y направлена ​​вниз, а Х – вправо, поэтому  верхний левый угол соответствует изображению в точке (0, 0).

Надо помнить, что у некоторых андроид системах память будет работать только с текстурами, стороны которых кратны 2 в степени n. Поэтому нужно стремиться, чтобы атлас с текстурами был соответствующих размеров в пикселях, например, 512х512 или 1024х512. Также, если вы не будете использовать POT текстуру (POT – Power Of Two, то есть степень двойки), вы не сможете применить тайлинг или автоматическую генерацию мипмапов. В данном случае под тайлингом понимается многократное повторение одной текстуры. Помните, правый нижний угол всегда имеет координаты (1,1), даже если ширина в два раза больше высоты. Это называется нормализованными координатами.
  В приложениях нередко используется множество маленьких текстур, причём переключение с одной текстуры на другую является относительно медленным процессом. Поэтому в подобных ситуациях бывает целесообразно применение одного большого изображения вместо множества маленьких. Такое изображение называют текстурный атлас (англ. Texture atlas). Под-текстуры отображаются на объект, используя UV-преобразование, при этом координаты в атласе задают, какую часть изображения нужно использовать.
Так как в нашем приложении есть три текстуры (небо, море и дельфин), они объединены в один атлас размером 1024х1024 формата png.

Как вы видите, я добавил еще одно изображение дельфина (в правом нижнем углу). Потом можно поиграться, и подключить его вместо того, что слева. Данный атлас сделан очень плохо, так как осталось много свободного места. Существуют алгоритмы и программы, которые позволяют упаковать изображения оптимальным образом. Например, как на фото.


Вес ( размер занимаемой памяти ) текстуры можно определить таким способом:
байт умножить на пиксель высоты и пиксель ширины, таким образом 32-битная текстура размера 1024 на 1024 занимает 4*1024*1024 = 4’194’304 байт.
16-битная текстура 1024 на 1024 займет уже только 2Мб, так что стоит подумать – стоит ли использовать 32-битные изображения или нет.
Существует аппаратное сжатие текстур, которое обычно позволяет в четыре раза уменьшить вес текстур. Однако, сейчас эти вопросы не главные, просто попутная информация к размышлению.
 В данном уроке будем использовать только метод GL_TEXTURE_2D, который позволяет надеть текстуру на плоскость (есть еще GL_TEXTURE_CUBE_MAP, который работает с текстурой развернутого куба, состоящей из 6-ти квадратов).

Как надеть текстуру?
Прежде чем надевать, выясним на что.
Один прямоугольник (состоящий из двух треугольников) будет лежать в плоскости х0у, на него наденем текстуру неба. Для этого в методе  private void prepareData() класса OpenGLRenderer создадим массив координат float[] vertices , куда занесем координаты не только треугольников, но и координаты соответствующей текстуры.
//coordinates for sky
 -2, 4, 0,   0, 0,
 -2, 0, 0,   0, 0.5f,
  2, 4, 0,   0.5f, 0,
  2, 0, 0,   0.5f, 0.5f,
Первые три числа строки - координаты левого верхнего угла неба  (-2, 4, 0), следующие два числа – координаты точки текселя (0,0), которые соответствуют нашей вершине треугольника. Обратите внимание на вторую точку (вторая строка), которая совпадает с левым нижним краем неба  (-2, 0, 0), для нее координаты точки текселя   (0, 0.5f), т.е.  s = 0 (левый край текселя), а t = 0.5, так как текстура неба занимает по вертикали только половину текселя.  Потом задаем третью точку (верхний правый край неба) и четвертую точку, чтобы нарисовать два треугольника методом GL_TRIANGLE_STRIP (смотри предыдущий урок).
Вторую плоскость (море) я сначала решил сделать перпендикулярно первой (небу), но потом несколько увеличил угол, понизив переднюю кромку моря для красоты фронтального вида на устройстве.
//coordinates for sea
  -2, 0, 0,       0.5f, 0,
  -2, -1, 2,      0.5f, 0.5f,
   2, 0, 0,       1, 0,
   2,-1, 2,        1, 0.5f,
Обратите внимание, как изменились координаты, вырезающие нам из атласа море.
Изображение дельфина я разместил на плоскость, которая параллельна небу и сдвинута на нас по оси 0Z на 0,5 единиц.
//coordinates for dolphin
-1, 1, 0.5f,      0, 0.5f,
-1, -1, 0.5f,      0, 1,
 1,  1, 0.5f,      0.5f, 0.5f,
 1, -1, 0.5f,       0.5f, 1,
Если возникнет желание поменять дельфина на другого, то это нужно сделать именно здесь.
Итак, мы прошли первый шаг и сделали соответствие между вершинами треугольников и точками текселя.

Второй шаг или, как загружаются текстуры
 Прежде, чем описать загрузку текстур, нужно разобраться с таким понятием, как текстурный слот. Именно к нему мы подключаем текстуру, с помощью его можем производить с ней разные манипуляции и менять её параметры.
Выбрать текущий слот для работы можно так:
 GLES20.glActiveTexture(GLES20.GL_TEXTUREx);
 где GLES20.GL_TEXTUREx – номер выбранного слота, например GLES20.GL_TEXTURE0,
константы прописаны для 32 текстур ( последняя GL_TEXTURE31 ).
 Для подключения текстуры к слоту используется процедура
 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture_id);
 Где: первый параметр – тип текстуры, второй – ссылка на текстуру.
 Эта процедура прикрепляет текстуру к текущему слоту, который был выбран до этого процедурой GLES20.glActiveTexture().
 То есть для того чтобы прикрепить текстуру к определенному слоту нужно вызвать две процедуры:
 GLES20.glActiveTexture(Номер_Слота);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, ссылка_на_текстуру);
 Важно помнить, что нельзя подключить одну текстуру одновременно к нескольким слотам.
Если вы ее переключили на другой слот и не установили текстуру для слота, к которому она была подключена ранее, то попытка прочесть скорей всего приведет к падению приложения. 
Как только мы поместили наш графический файл texture.png в папку ресурсов проекта drawable, система автоматически присвоила ему номер id (идентификатор ресурса - это целое число, которое является ссылкой на данный ресурс). Идентификаторы ресурсов хранятся в  файле R.java.
В классе TextureUtils есть метод loadTexture. Этот метод принимает на вход id ресурса картинки, а на выходе возвращает id созданного объекта текстуры, который будет содержать в себе эту картинку.
Итак, вначале передаем в качестве аргументов контекст и идентификатор ресурса графического файла
public static int loadTexture(Context context, int resourceId) {
 Потом создаем пустой массив из одного элемента. В этот массив OpenGL ES запишет свободный номер текстуры,  который называют именем текстуры textureIds
final int[] textureIds = new int[1];
     Потом генерируем свободное имя текстуры, которое будет записано в textureIds[0]
glGenTextures(1, textureIds, 0); 
    Первый параметр определяет, как много объектов текстур мы хотим создать. 
Обычно создаем всего одну. Следующий параметр — имя текстуры, куда OpenGL ES 
будет записывать id сгенерированных объектов текстур. Последний параметр просто 
сообщает OpenGL ES, с какой точки массива нужно начинать записывать id.
Проверяем, если ничего не записано, то возвращаем ноль.
if (textureIds[0] == 0) { return 0; }
   Флаг inScaled включен по умолчанию и должен быть выключен, если нам нужна не масштабируемая версия растрового изображения.  
  final BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;
Загружаем картинку в Bitmap из ресурса
final Bitmap bitmap = BitmapFactory.decodeResource(
        context.getResources(), resourceId, options);
      Объект текстуры по-прежнему пуст. Это значит, что у него по-прежнему нет никаких графических данных. Загрузим наше растровое изображение. Для этого нам необходимо сначала привязать текстуру. В OpenGL ES под привязкой чего-либо понимается, что мы хотим, чтобы OpenGL ES использовал данный конкретный объект для всех последующих вызовов до тех пор, пока мы снова не изменим привязку. В данном случае мы хотим привязать текстуру объекта. Для этого мы используем метод glBindTexture(). Как только привяжем текстуру, сможем управлять ее свойствами, такими как данные об изображении.
Выбираем активный слот текстуры
glActiveTexture(GL_TEXTURE0);
Делаем текстуру с именем textureIds[0] текущей
glBindTexture(GL_TEXTURE_2D, textureIds[0]);
Создаем прозрачность текстуры. Если не написать эти две строки, наш дельфин будет
на черном непрозрачном фоне, как на скриншоте выше.
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
GLES20.glEnable(GLES20.GL_BLEND);
    Есть еще одна деталь, которую нам необходимо определить перед тем, как мы сможем использовать объект текстуры. Она связана с тем, что наш треугольник может занимать больше или меньше пикселов на экране по сравнению с тем, сколько пикселов есть в обозначенной зоне текстуры. Например, на экране мы можем использовать гораздо больше пикселов по сравнению с тем, что перенесли из зоны текстуры. Естественно, может быть и наоборот: мы используем меньше пикселов на экране, чем на выделенной зоне текстуры. Первый случай называется магнификацией, а второй — минификацией. В каждом из них нам необходимо сообщить OpenGL ES, как нужно увеличивать или уменьшать текстуру. В терминологии OpenGL ES соответствующие механизмы называются фильтрами минификации и магнификации. Эти фильтры являются свойствами объекта текстуры, как и сами данные изображения. Чтобы их установить, необходимо сначала проверить, привязан ли объект текстуры с помощью glBindTexture(). Если это так, устанавливаем их следующим образом:
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.
GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.
GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
О влиянии фильтров на изображение можно почитать здесь
http://www.learnopengles.com/tag/mipmap/ 
 Переписываем Bitmap в память видеокарты
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
Удаляем Bitmap из памяти, т.к. картинка уже переписана в видеопамять
bitmap.recycle();
    И напоследок снова вызываем метод glBindTexture, в котором в слот текстуры GL_TEXTURE_2D передаем 0. Тем самым, мы отвязываем наш объект текстуры от этого слота.
glBindTexture(GL_TEXTURE_2D, 0);
return textureIds[0];
 Еще раз, мы сначала разместили объект текстуры в слот GL_TEXTURE_2D,
glBindTexture(GL_TEXTURE_2D, textureIds[0]);
 потом выполнили все операции с ним, затем освободили слот. В результате объект текстуры у нас теперь настроен, готов к работе, и не привязан ни к какому слоту текстур.

Доступ к текстурам из шейдера 
В предыдущих уроках мы писали шейдеры в самом теле программы в виде объектов строки. Удобно вынести их в отдельный ресурс, как предложил
Таки образом, в папке проекта res создается папка raw, в которую закладываются два файла
vertex_shader.glsl и fragment_shader.glsl.
Вот их содержимое
vertex_shader.glsl

attribute vec4 a_Position;
uniform mat4 u_Matrix;
attribute vec2 a_Texture;
varying vec2 v_Texture;
void main()
{
    gl_Position = u_Matrix * a_Position;
    v_Texture = a_Texture;
}
      Здесь мы, как и ранее, вычисляем итоговые координаты (gl_Position) для каждой
 вершины с помощью матрицы. А в атрибут a_Texture у нас приходят данные по 
координатам текстуры, которые мы сразу пишем в  varying переменную v_Texture. 
Это позволит нам в фрагментном шейдере получить интерполированные данные 
по координатам текстуры.
 fragment_shader.glsl

precision mediump float;
uniform sampler2D u_TextureUnit;
varying vec2 v_Texture;
void main()
{
    gl_FragColor = texture2D(u_TextureUnit, v_Texture);
}
Вначале устанавливаем среднюю точность расчетов precision mediump float;
В GLSL существует специальный тип униформы, который называется sampler2D. Сэмплеры можно объявлять только во фрагментном шейдере
 uniform sampler2D u_TextureUnit;
 В нем у нас есть uniform переменная u_TextureUnit, в которую мы получаем номер слота тектуры, в котором находится нужная нам текстура. Обратите внимание на тип переменной. Напомню, что из приложения мы в эту переменную передавали 0, как integer. Т.е. переданное в шейдер число (а нашем случае 0) указывает на какой слот текстуры смотреть. 
В varying переменную v_Texture приходят интерполированные координаты текстуры из вершинного шейдера. И шейдер знает, какую точку текстуры надо отобразить в текущей точке треугольника.
Осталось использовать координаты текстуры и саму текстуру, чтобы получить итоговый фрагмент. Это выполнит метод texture2D, и в gl_FragColor мы получим цвет нужной точки из текстуры.
 Исходники скачиваем отсюда

Удачи вам и всего хорошего!

Основные источники:






Комментариев нет:

Отправить комментарий