4 апреля 2012 г.

OpenGL ES 1. Двумерные текстуры

Двумерная текстура представляет из себя прямоугольное графическое изображение, загруженное в память OpenGL. Текстура используется для нанесения изображения на поверхность, составленную из полигонов (многоугольников). Двумерная текстура имеет две координаты, горизонтальную координату принято обозначать буквой S, вертикальную - буквой T. Координаты текстуры нормированы на единицу, т.е. принимают значения от 0 до 1. Левый верхний угол картинки имеет координаты S=0, T=0, правый нижний угол - S=1, T=1:

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

Спроецируем углы текстуры на вершины квадрата с длиной стороны равной двум и координатами X и Y в диапазоне от -1 до 1:
















Возьмем в качестве примера текстуру, представляющую из себя квадрат с кольцами. Получим такое изображение:
Теперь попробуем изменить правило присвоения вершинам координат текстуры. Правой нижней точке нашего квадрата присвоим координаты текстуры S=0.75, T=0.75, а левой верхней точке S=0.25, T=0.25:

Получим такое изображение, растянутое по диагонали:














Данный пример показывает, присвоение координат текстуры вершинам в общем случае может осуществляться по индивидуальным законам, определенным самим разработчиком. Например, изменяя координаты текстуры вершины, можно добиться таких эффектов как отражение и преломление света. Координаты текстуры можно привязать к чему угодно: к координатам вершин или к вектору нормали по определённым правилам. Правило привязки устанавливаете Вы сами. 
Вернёмся от теоретических рассуждений к практике. Начнем с вопроса: "Как загрузить графический файл в OpenGL ES ? Сначала мы должны указать программе как упакованы пиксели в файле. Обычно принимают за правило, что одному цвету (красному, зелёному и синему) соответствует один байт в файле. Устанавливается это так:
gl.glPixelStorei(GL10.GL_UNPACK_ALIGNMENT,1);
Если забудете выполнить данную команду можете получить изображение разрезанное на куски по диагонали.
Далее нужно попросить OpenGL выделить нам свободные имена текстур. Имя текстуры-это уникальный номер текстуры, по которой происходит обращение к ней. Что представляет из себя имя текстуры ? Обычное целое число - 1, 2, 3 и.т.д. Суть в том, что для привязки новой текстуры к имени оно должно быть свободно, т.е. не занято другой текстурой. Для генерации свободных имен существует команда glGenTextures, формат которой выглядит так:
gl.glGenTextures(количество создаваемых имен, массив в который нужно записать имена, 0);
Например, нам нужно получить одно свободное имя для загрузки текстуры. Мы должны предварительно создать пустой массив целых чисел, куда будут записаны имена. Поскольку необходимо одно имя- размер этого массива равен единице:
int[] names=new int[1];
Теперь получим в этот массив одно свободное имя текстуры:
gl.glGenTextures(1, names, 0);
Мы можем обращаться к полученному имени как к names[0]. Далее, мы должны сделать наше имя текстуры текущим поскольку все последующие команды будут действовать на это имя. Установка текущей текстуры производится командой:
gl.glBindTexture(GL10.GL_TEXTURE_2D, names[0]);
Первый аргумент указывает, что текущая текстура будет двумерной. В прочем, бывают ещё одномерные, трёхмерные и кубические текстуры. В этой статье мы их рассматривать не будем. Второй аргумент-это имя нашей текстуры. 
Теперь можно настраивать индивидуальные параметры текущей текстуры. Создадим мипмапы для нашей текстуры. Что такое мипмап ? Это множество уменьшенных копий нашего изображения. Они нужны для того, чтобы текстуры удалённых от глаза наблюдателя объектов выглядели плавными без резких переходов. Не забудьте сгенерировать мипмапы, иначе получите неприятный для глаза эффект, при котором на изображении дальних текстур будут чётко выделяться движущиеся полосы. В расширение OpenGL ES 1.1 включена команда для автоматической генерации мипмапов всех размеров:
gl.glTexParameterx(GL11.GL_TEXTURE_2D,
          GL11.GL_GENERATE_MIPMAP,
          GL11.GL_TRUE);
Текстура состоит из элементов-пикселей. Пиксели текстуры принято называть текселами. Изображение полигона на экране тоже состоит из пикселей. При проецировании текстуры на полигон возможны две ситуации: когда один тексел попадает на несколько пикселей полигона (увеличение текстуры) и когда на один пиксель приходится несколько текселов (уменьшение текстуры). Сразу возникает вопрос: "какой из текселов будет присвоен данному пикселю при увеличении или уменьшении текстуры?". Процедура выбора наиболее подходящего тексела для данного пикселя называется фильтрацией текстур. Также для установки фильтра может выполняться интерполяция цветов нескольких текселов при записи в пиксель. Для фильтрации двумерных текстур используется команда glTexParameterf
Первый аргумент данной команды определяет тип текстуры, в нашем случает текстура двумерная поэтому используется GL_TEXTURE_2D. Второй аргумент устанавливает режим для которого применяется фильтр: GL_TEXTURE_MIN_FILTER при уменьшении или GL_TEXTURE_MAG_FILTER при увеличении текстуры.
В режиме уменьшения текстуры существует возможность использовать мипмапы при установке фильтра. При этом наилучшее качество изображения получается при использовании GL_LINEAR_MIPMAP_LINEAR в качестве третьего аргумента команды glTexParameterf:
gl.glTexParameterf(GL10.GL_TEXTURE_2D, //указывает, что используется двумерная текстура
      GL10.GL_TEXTURE_MIN_FILTER, // указывает, что установка действует при уменьшении
      GL10.GL_LINEAR_MIPMAP_LINEAR); //установленное правило фильтрации
В режиме увеличения мип-мапы не используются, а применяется только основное, самое крупное изображение текстуры. Наилучшее качество при увеличении текстуры обеспечивает третий аргумент GL_LINEAR
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
     GL10.GL_TEXTURE_MAG_FILTER,
     GL10.GL_LINEAR);

Что произойдет если координаты текстуры выйдут за пределы диапазона от 0 до 1 ? Для управления выводом ненормированных координат текстур существует два режима так называемого "обёртывания" (wrap). По умолчанию целая часть текстурной координаты будет отброшена и оставлена только дробная часть. Например, если координата текстуры равна 2.7, то при проецировании текстуры на полигон она отобразится как 0.7. Таким образом, к границе текстуры как-бы "приклеивается" ёё копия, которая повторяется по количеству целых значений в координате S или T. Такой режим называется повтором (GL_REPEATи задается командами :
gl.glTexParameterx(GL10.GL_TEXTURE_2D,
     GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT); 
// для координаты S
gl.glTexParameterx(GL10.GL_TEXTURE_2D,
     GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT); // для координаты T
При повторе необходимо обеспечить плавный переход между концом одного экземпляра текстуры и началом другого, т.е. текстура должна быть бесшовной. Кроме повтора изображения существует режим GL_CLAMP_TO_EDGE, при котором крайние текселы текстуры подтягиваются к границе полигона. 
После установки параметров текстуры мы можем приступить к загрузке изображения из графического файла. Сначала создадим объект класса Bitmap и загрузим в него картинку из ресурсов Android:
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(),idpicture);
где context-текущий контекст, idpicture - целочисленный идентификатор графического ресурса из файла R.java. Графические файлы обычно располагают в каталоге res в подкаталогах drawable-hdpi, drawable-ldpi и drawable-mdpi. Далее Android SDK автоматически присваивает им идентификаторы и записывает их в файл R.java. Например, если нам нужно получить идентификатор файла icon1.jpg мы можем использовать такое обращение к R.java: idpicture=R.drawable.icon1
Затем мы должны предать загруженный файл в OpenGL для формирования из него двумерной текстуры. Для этого существует команда:
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
bitmap нам больше не нужен, т.к. из него уже сформирована текстура и его можно удалить:
bitmap.recycle();
Графическое изображение, а также правила фильтрации и "обёртывания" привязаны к имени текущей текстуры и являются её свойствами. Поэтому удобно определить отдельный класс и создавать текстуры в виде объектов этого класса.

Далее рассмотрим вопрос взаимодействия текстуры и освещения. При наложении текстуры на полигон освещение будет игнорировано если выполнена команда:
gl.glTexEnvx(GL10.GL_TEXTURE_ENV,
       GL10.GL_TEXTURE_ENV_MODE,
       GL10.GL_REPLACE);
При использовании третьего аргумента GL_REPLACE текстура полностью перезаписывает пиксели на полигоне. Поэтому эффект освещения исчезает. Чтобы совместить освещение с текстурой нужно передать в качестве третьего аргумента параметр GL_MODULATE:
gl.glTexEnvx(GL10.GL_TEXTURE_ENV,
       GL10.GL_TEXTURE_ENV_MODE,
       GL10.GL_ MODULATE ); 
В этом случае цвет связанный с освещением умножается на цвет текстуры в данной точке изображения и эффект освещения становится заметным. Нужно отметить, что команда glTexEnvx не привязывает свои настройки к имени конкретной текстуры. Поэтому она не может входить в класс текстуры и должна вызываться каждый раз перед рисованием объекта.

Теперь мы должны научиться передавать координаты текстур в OpenGL по аналогии с координатами вершин. Рассмотрим это на примере нашего квадрата. Сначала подготовим буфер необходимой длины. Каждой вершине присвоены две текстурные координаты S и T. Каждая координата занимает 4 байта в памяти, итого 8 байт на вершину, 32 байта на квадрат:
ByteBuffer bb = ByteBuffer.allocateDirect(32);
bb.order(ByteOrder.nativeOrder());
texcoordBuffer = bb.asFloatBuffer();
 
Запишем координаты текстуры в буфер
texcoordBuffer.position(0); 
texcoordBuffer.put(0); // координата S левого нижнего угла
texcoordBuffer.put(1); // координата T левого нижнего угла 
texcoordBuffer.put(0.75f);   // координата S правого нижнего угла 
texcoordBuffer.put(0.75f);  // координата T правого нижнего угла  
texcoordBuffer.put(1);  // координата S правого верхнего угла 
texcoordBuffer.put(0);  // координата T правого верхнего угла  
texcoordBuffer.put(0.25f);   // координата S левого верхнего угла 
texcoordBuffer.put(0.25f);   // координата T левого верхнего угла  
texcoordBuffer.position(0);
Запись в буфер можно также производить из заранее подготовленного массива с координатами текстур. Например так:
float [] texcoordBufferArray={0,1,  0.75f,0.75f,  1,0,  0.25f,0.25f};
texcoordBuffer.position(0); 
texcoordBuffer.put(texcoordBufferArray);
texcoordBuffer.position(0); 
Нужно обязательно соблюдать следующее правило: порядок обхода вершин при записи в буферы должен быть одинаков для координат вершин, векторов нормалей и координат текстур. При этом изображение будет корректным. После того как в буфер переданы координаты текстуры для вершин мы должны включить использование двумерных текстур командой:
gl.glEnable(GL10.GL_TEXTURE_2D);
Выбираем текущую активную текстуру с именем names[0]:
gl.glBindTexture(GL10.GL_TEXTURE_2D, names[0]);
Если необходимо, чтобы при текстурировании учитывалось освещение, нужно включить режим модуляции для текущей текстуры:
gl.glTexEnvx(GL10.GL_TEXTURE_ENV,
       GL10.GL_TEXTURE_ENV_MODE,
       GL10.GL_ MODULATE );
Разрешаем использование массивов для координат текстур:
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
Затем передаем буфер с координатами текстуры в OpenGL:
gl.glTexCoordPointer(2,GL10.GL_FLOAT,0,texcoordBuffer);

Данные с координатами текстуры переданы в OpenGL и будут учтены при следующем вызове команды glDrawArrays, которая нарисует полигон.

7 комментариев:

  1. Этот комментарий был удален автором.

    ОтветитьУдалить
  2. Этот комментарий был удален автором.

    ОтветитьУдалить
  3. Этот комментарий был удален автором.

    ОтветитьУдалить
  4. Этот комментарий был удален автором.

    ОтветитьУдалить
  5. Подскажите пожалуйста: как можно реализовать эффект hue/saturation/brightness для текстуры или для всего экрана на OpenGL 1.0? И можно ли проект переделать на OpenGL 2.0 с 1.0? Или какие то команды устарели на 1.0 и придется переписывать код?

    ОтветитьУдалить
  6. Тут немного по интернету пошарил, нашел что такое можно сделать шейдерами, но пока разобраться не могу и мне кажется медленно будет. Я вот думаю может как то из буфера текстуру брать обрабатывать каждый пиксель и обратно вставлять?

    ОтветитьУдалить