Sally’s Spa iDroidsMania - роботы тоже могут любить…
Июн 21

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

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

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

Как уже упоминалось, основная часть кода относится к загрузке текстур в программу и непосредственно в OpenGL для последующего использования. Ничего сложного, но поработать с iPhone SDK придется немало.

Готовимся к текстурам

Перед тем, как использовать текстуру, предварительно ее нужно загрузить в приложение, отформатировать для OpenGL и сообщить, где ее искать. Остальное будет ничуть не сложнее раскрашивания квадрата в прошлом уроке.

Запустить Xcode и откройте в редакторе файл “EAGLView.h“. Для начала предоставим OpenGL необходимую переменную. Добавьте следующую декларацию:

1
GLuint textures[1];

Думаю, ясно, что это массив “1 GLuint.“. Ране уже приводился пример использования “Glfloat“, а ” GLuint” — это тип typedef в OpenGL для целых чисел без знака. Типы GLxxxx всегда применяются здесь вместо типов Objective C, поскольку конструкции typedef в OpenGL определены для реализации, а не для разработки.

Позже для заполнения переменной мы вызовем предусмотренную в OpenGL функцию “glGenTextures()“. Пока просто запомните, что она определена, а к функции “glGenTextures()” и самой переменной мы вернемся ниже.

Внизу, в прототипах метода, добавьте следующий прототип:

1
- (void)loadTexture;

Вот сюда мы и поместим код для загрузки текстуры.

Добавляем к проекту фреймворк CoreGraphics

Чтобы загрузить и обработать текстуру, воспользуемся фреймворком CoreGraphics, содержащим все необходимые методы — нам не придется прописывать код нижнего уровня, как в уроках по Windows OpenGL.

В разделе “Groups & Files” редактора Xcode щелкните правой кнопкой на группе “Frameworks” и выполните “Add -> Existing Frameworks…“.

В окне поиска наберите “CoreGraphics.framework” и найдите в результатах папку, соответствующую нужному приложению (в моем случае это iPhone SDK 2.2.1). Щелкнув на папке, добавьте ее к проекту (пиктограмма папки для фреймворка — это нормально).

Следующим шагом внесем в проект изображение текстуры, включив его в bundle приложения. Загрузите текстуру “checkerplate.pngздесь и сохраните в папку проекта. Добавьте изображение к относящейся к проекту группе “Resources:” щелкнув по опции “Resources” правой кнопкой мыши, выберите “Add -> Existing Files…“. Выберите изображение, которое должно появиться в соответствующей группе.

Загружаем текстуру в приложение и в OpenGL

Переключаемся на файл “EAGLView.m” для реализации метода “loadTexture“.

1
2
- (void)loadTexture {
}

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

1
2
3
4
5
CGImageRef textureImage = [UIImage imageNamed:@"checkerplate.png"].CGImage;
if (textureImage == nil) {
NSLog(@"Failed to load texture image");
return;
}

CGImageRef” — тип данных CoreGraphics, собирающий всю информацию по изображению. Для ее получения используем метод класса “UIImage” с названием “imageNamed:“. Он создает автоматически высвобождаемый “UIImage” с поиском файла по имени в основном bundle приложения. “UIImage” автоматически создает “CGImageRef” и доступен через его свойство “CGImage“.

Теперь получаем размер изображения, на который будем ссылаться ниже:

1
2
NSInteger texWidth = CGImageGetWidth(textureImage);
NSInteger texHeight = CGImageGetHeight(textureImage);

Данные “CGImageRef” содержат ширину и высоту изображения, но прямого доступа к ним нет: понадобятся два указанных выше средства доступа.

CGImageRef” содержит не информацию по изображению, а лишь ссылку на нее (как несложно догадаться из названия). Следовательно, для хранения этих данных нужно выделить память:

1
GLubyte *textureData = (GLubyte *)malloc(texWidth * texHeight * 4);

Чтобы точно подсчитать объем выделяемой памяти, умножьте ширину на высоту, а полученное значение на 4. Надеюсь, из прошлого урока вы помните, что OpenGL принимает только значения RGBA? Размер пикселя составляет 4 байта, по байту на каждое из значений RGBA.

Теперь нас ждет целая куча запросов функций:

1
2
3
4
5
6
7
8
9
10
11
12
13
CGContextRef textureContext = CGBitmapContextCreate(
textureData,
texWidth,
texHeight,
8, texWidth * 4,
CGImageGetColorSpace(textureImage),
kCGImageAlphaPremultipliedLast);

CGContextDrawImage(textureContext,
CGRectMake(0.0, 0.0, (float)texWidth, (float)texHeight),
textureImage);

CGContextRelease(textureContext);

Первой идет функция CoreGraphics, возвращающая контекстную ссылку на графику Quartz2D. Фактически, здесь мы указываем CoreGraphics на данные текстуры, сообщаем ее формат и размер.

После этого мы рисуем изображение в находящихся в памяти данных (ссылка textureData), отталкиваясь от информации, указанной созданной ранее контекстной ссылкой. Контекст содержит все необходимое для копирования данных в выделенное посредством “malloc()” пространство, причем в корректном для OpenGL формате.

Собственно говоря, с CoreGraphics мы закончили и можем освободить собственноручно созданный дескриптор “textureContext“.

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

Переходим к программированию с OpenGL.

Помните задекларированную в заголовочном файле переменную? Пришло время с ней поработать. Взгляните на следующую строку кода:

1
glGenTextures(1, &textures[0]);

Мы скопируем данные текстуры из приложения в OpenGL, поэтому должны сообщить OpenGL о необходимости выделить для них память (напрямую сделать этого нельзя). Если помните, “textures[]” мы определили как “GLuint“. После вызова “glGenTexturesOpenGL создает “дескриптор” или “указатель”, являющийся уникальной ссылкой на каждую загруженную в OpenGL отдельную текстуру. Возвращаемое OpenGL значение нам не важно. Просто каждый раз, когда необходима ссылка на текстуру “checkerplate.png“, ссылаемся на “textures[0]“. И мы, и OpenGL знаем, о чем идет речь.

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

1
2
GLuint textures[10];
glGenTextures(10, &textures[0]);

Для нашего примера достаточно одной текстуры, поэтому и памяти выделяем соответствующий объем.

Теперь нужно активировать только что созданную текстуру:

1
glBindTexture(GL_TEXTURE_2D, textures[0]);

Второй параметр очевиден — это созданная нами текстура. Первым параметром всегда является “GL_TEXTURE_2D“, поскольку на данном этапе OpenGL ES принимает только ее. “Полноценная” OpenGL поддерживает одно- и трехмерные текстуры. Я уверен, что OpenGL ES для совместимости в перспективе это необходимо.

Запомните, что текстуры активируются именно таким образом.

Следующим шагом отправляем OpenGL наши данные по текстуре (на которые ссылается “textureData“). OpenGL будет обрабатывать их “у себя” (в серверном режиме), преобразуя данные в нужный формат для аппаратной реализации и копируя в свое пространство. Информации много, но основная часть параметров всегда одинакова — ввиду ограничений OpenGL ES:

1
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texWidth, texHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, textureData);

Разберем параметры:

  • target — как правило, всегда GL_TEXTURE_2D.
  • level — определяет уровень детализации текстуры. 0 — максимальная допустимая изображением детализация, с увеличением значения происходит уменьшение изображения n-ного уровня множественного отображения.
  • internal_format —внутренний формат и представленный ниже параметр “format” должны быть одинаковы. Следовательно, для обоих ” GL_RGBA”.
  • width — ширина изображения.
  • height — высота изображения.
  • border — всегда устанавливается на 0, поскольку OpenGL ES не поддерживает границы текстур.
  • format — должен быть таким же, как “internal_format”.
  • type — тип каждого пикселя. Помните, что на пиксель у нас приходится 4 байта? Соответственно, каждый пиксель состоит из одного байта без знака (RGBA).
  • pixels — указывает на фактические данные по изображению.

Как видим, параметров не так уж и много. Основная их часть либо определяется логическим путем, либо всегда неизменна, либо требует ввода указанных ранее переменных (textureData, texWidth и texHeight). Помните, что вы лишь передаете OpenGL контроль над информацией по текстурам.

Теперь, предоставив данные OpenGL, можно освободить из памяти “textureData“.

1
free(textureData);

Осталось вызвать всего три функции:

1
2
3
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glEnable(GL_TEXTURE_2D);

Три этих запроса осуществляют последние настройки в OpenGL и переводят ее в “состояние” наложения текстур.

Два первых обращения сообщают OpenGL о способе обработке текстур при увеличении (приближении — “GL_TEXTURE_MAG_FILTER“) и уменьшении (отдалении — “GL_TEXTURE_MIN_FILTER“). Для наложения текстур необходимо задать как минимум одну из них, а опция “GL_LINEAR” указана для обеих.

Мы вызываем “glEnable()“, чтобы OpenGL использовала текстуры, когда в коде рисования мы об этом попросим.

Последним шагом нужно добавить запрос данного метода в инициализаторе “initWithCoder“.

1
2
[self setupView];
[self loadTexture];// Добавьте эту строку

Просто добавьте вторую строку после обращения к методу “setupView“.

Корректировки в “drawView”

Все самое сложное уже сделано. Отредактировать метод “drawView” не сложнее, чем раскрасить квадрат из предыдущего урока. Первым делом откомментируйте массив “squareColours[]“, поскольку нам он не понадобится.

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

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

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

Отсылаем обратно на “squareVertices[]

1
2
3
4
5
6
const GLfloat squareVertices[] = {
-1.0, 1.0, 0.0,               // Верхняя левая
-1.0, -1.0, 0.0,              // Нижняя левая
1.0, -1.0, 0.0,               // Нижняя правая
1.0, 1.0, 0.0                 // Верхняя правая
};

Обратили внимание, что первая указываемая координата текстуры — для верхней левой вершины? Координата текстуры (0, 1). Вторая вершина — верхний правый угол квадрата. Следовательно, координатой для нее будет (1, 1). Переходим к нижнему правому углу с координатой текстуры (1, 0) и, наконец, заканчиваем левым нижним углом с координатой текстуры (0, 0). Таким образом, мы определили “squareTextureCoords[]” следующим образом:

1
2
3
4
5
6
const GLshort squareTextureCoords[] = {
0, 1,       // верхняя левая
0, 0,       // нижняя левая
1, 0,       // верхняя правая
1, 1        // нижняя правая
};

Обратите внимание: мы использовали “GLshort” вместо “GLfloat“. Добавьте представленный выше код к проекту.

Думаю, четко просматривается аналогия с нашими действиями с массивом цветов.

Все верно: теперь нужно отредактировать код рисования. Код рисования треугольника оставляем, как есть, и начинаем с “glLoadIdentity()” перед кодом рисования квадрата. Теперь он должен выглядеть следующим образом:

1
2
3
4
5
6
7
8
9
10
11
12
glLoadIdentity();
glColor4f(1.0, 1.0, 1.0, 1.0);      // НОВОЕ
glTranslatef(1.5, 0.0, -6.0);
glRotatef(rota, 0.0, 0.0, 1.0);
glVertexPointer(3, GL_FLOAT, 0, squareVertices);
glEnableClientState(GL_VERTEX_ARRAY);

glTexCoordPointer(2, GL_SHORT, 0, squareTextureCoords);     // НОВОЕ
glEnableClientState(GL_TEXTURE_COORD_ARRAY);                // НОВОЕ

glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);               // НОВОЕ

Итак, перед нами четыре новых строки кода. Код раскрашивания квадрата из предыдущего урока я удалил. Первая строка — вызов функции “glColor4f()“, которую я подробно рассмотрю ниже.

Следующие три строки нам уже хорошо известны. Вместо ссылки на вершины объекта или цвета мы ссылаемся на текстуры:

1
2
glTexCoordPointer(2, GL_SHORT, 0, squareTextureCoords);     // НОВОЕ
glEnableClientState(GL_TEXTURE_COORD_ARRAY);                //НОВОЕ

Первый запрос сообщает OpenGL, где хранится массив координат текстур и в каком формате. Отличие в том, что мы передаем по два значения для каждой координаты (ведь это, само собой, двухмерная текстура). Использованный тип данных “GLushort” здесь преобразуется в “GL_SHORT“. Шага по индексу “stride” нет (0), зато есть ссылка на наши координаты.

Следующим шагом переводим OpenGL в клиентское состояние для наложения текстур на основании только что описанного массива координат.

Неизмененная “glDrawArrays()” вызывается перед:

1
glDisableClientState(GL_TEXTURE_COORD_ARRAY);               // НОВОЕ

Помните, как мы отключали массив цветов, когда хотели раскрасить квадрат иначе, чем треугольник? Так мы поступим и с текстурой, или OpenGL использует ее для треугольника.

Внеся изменения в код, щелкните на кнопке “Build and Go“, получив следующий результат:

Наша рифленая текстура теперь наложена на квадрат, а треугольник остался тем же.

Дальнейшие эксперименты

Для начала обсудим новую строку “glColor4f()“, добавленную к коду рисования квадрата:

1
glColor4f(1.0, 1.0, 1.0, 1.0);      // НОВОЕ

Разумеется, при этом цвет рисования меняется на полностью непрозрачный белый. Догадались, зачем я добавил эту строку? Вспоминаем, что OpenGL остается в заданном “состоянии”, пока не будет указано иное. Поэтому цвет был синим, пока мы не сделали его белым.

При наложении текстур OpenGL выполняет умножение текущего цветового набора (синего) на текущий пиксель текстуры. То есть:

R     G      B       A
Цвет:                                               0.0, 0.0, 0.8, 1.0
Цвет пикселя текстуры:     1.0, 1.0, 1.0, 1.0

При рисовании пикселя OpenGL перемножает

Colour_Red * Pixel_Colour_Red = Rendered_colour
0.0                    *                   1.0                             = 0.0
Colour_Green * Pixel_Colour_Green
0.0                     *                 0.0                              = 0.0
Colour_Blue * Pixel_Colour_Blue
0.8                      *                 1.0                              = 0.8

Откомментировав “glColor4f()” перед кодом рисования квадрата, получим примерно следующее:

В ситуации с белым умножение выглядит так:

Цвет: 1.0, 1.0, 1.0, 1.0
умножается на
Цвет пикселя: 0.8, 0.8, 0.8, 1.0

Результат: 0.8, 0.8, 0.8, 1.0

Вот поэтому цвет мы сделали белым.

Вот оно!

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

Исходный код скачать можно [здесь]

Текст оригинальной статьи на английском языке [здесь]

Уважаемые читатели, данный материал был переведен и подготовлен к публикации проектом LookApp.ru, при публикации на другом сайте ссылка на LookApp.ru обязательна.

1 звезда2 звезд3 звезд4 звезд5 звезд (8 голосов, средний: 5.00 из 5)
Загрузка ... Загрузка ...


10 Responses to “Уроки iPhone SDK: (Часть 6) OpenGL ES: накладываем текстуры на квадрат”

  1. 1. ikol Says:

    А есть какието ограничения на загрузку текстур? я имею виду общий объем текстур,одновременно загруженный в память. я гдето слышал о 8 мб.

  2. 2. Artem Says:

    Точно не знаю но вроде что-то такое встречал. Попробуйте поискать ответ в первых уроках из серии OpenGL ES

  3. 3. Kirill Says:

    Здравствуйте, очень хорошо все написано, почти все понятно, но) в строке glGenTextures(10, &textures[0]); ругается на &amp, что это за аргумент ваще? Может что-то необходимо подключить?

  4. 4. Kirill Says:

    ой вот в этой glGenTextures(1, &textures[0]);

  5. 5. Lexus Says:

    Аналогичная ошибка как и у Kirill. По возможности разъясните как ее обойти.

  6. 6. SWG admin Says:

    вместо этого
    glGenTextures(1, &textures[0]);
    используйте это
    glGenTextures(1, &textures[0]);

    P.S.
    просто мнемокод [&] используется в html для обозначения символа “&”

  7. 7. Artem Says:

    Опа. Это плохо, надо с этим как-то бороться. Вот почему ошибки у людей были.

  8. 8. SWG admin Says:

    Артем! не тот комментарий удалил - нужно было первый, а то не видна разница
    Этот правильный

    вместо этого
    glGenTextures(1, &textures[0]);
    используйте это
    glGenTextures(1, &textures[0]);

    P.S.
    просто мнемокод [&amp] используется в html для обозначения символа “&”

  9. 9. Artem Says:

    Ок, спасибо. Надо как то с этим бороться.

  10. 10. Artur Says:

    Здравствуйте! Такой вопрос. Я вот поменял картинку для текстуры (формат jpg, 240×320). А квадрат белый =) Что может быть? Заранее спасибо за ответ

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