|
Июн
29
|
Когда обучение в школе уже подходило к концу, в расписание закрался предмет по высшей математике, сравниться с которым по занимательности могло разве что наблюдение за сохнущей краской. Я со спокойной душой пропускал уроки, поскольку изучение столь продвинутых концепций с классом меня не вдохновляло.
Как-то раз я сидел на задней парте, размышляя о чем-то занимательном и не имея ни малейшего представления о том, что в тот день изучали. Меня это не интересовало, поскольку я знал, что могу освоить предмет, как только мне он понадобится. Но в тот день, посмотрев на доску, я был крайне удивлен, обнаружив на ней
[ a b c ] . [ d e f ]
Я так и сел. Я знал, что это математика матриц — читал об этом в литературе по графическим программам, но не совсем понял. Вдохновившись, я решил почтить урок своим вниманием и узнал массу интересного.
Вдохновение — великая вещь. Сколько ни пытался, у меня никак не получилось довести до ума урок по смешиванию (Часть 1). Поэтому я просто опубликовал его, решив вернуться к этой теме позже — когда посетит вдохновение.
Но дело в том, что изучение усложняющихся концепций OpenGL ES на примере куба и пирамиды меня вряд ли когда-нибудь вдохновит. Настало время (постепенно) увеличивать сложность сцен, с которыми мы экспериментируем — не давая иссякать вдохновению.
Наращивать сложность сцен мы будем, не спеша, давая возможность подтянуться всем желающим. Но делать это нужно, хотя бы для того, чтобы освоить управление сценами наряду с новыми концепциями OpenGL ES.
В этом уроке нам предстоит иметь дело с туннелем. Но перед тем как переходить непосредственно к сцене, решим кое-какие оставшиеся вопросы. Для начала рассмотрим дополнительный момент по загрузке текстур в iPhone, который я оставил до более удобного момента (читай: забыл). После этого нарисуем туннель, используя загруженные из одного файла четыре различные текстуры. Последним шагом выполним базовую визуализацию текстур и еще раз вернемся к смешиванию.
Приступаем. Здесь ссылка на исходный проект к уроку (готовый проект можно найти в конце).
Y в iPhone != Y в OpenGL
Координаты Y в OpenGL на многих платформах связаны с массой проблем, и iPhone не исключение. Когда для загрузки текстуры используется CoreGraphics, визуализация происходит “вверх ногами” (т.е. с поворотом на 180º по оси X).
Не стоит пытаться исправить это вызовом функции “glRotatef()“. На экране результат станет верным, но за счёт лишних ресурсов процессора.
Приемлемыми решениями можно считать следующие:
1. Предварительно обрабатываем в графическом редакторе текстуру для корректной загрузки. Подход работает, но снижает потенциал кросс-платформенной совместимости. Да и работы лишней добавляется. Не рекомендую.
2. Меняем массив координат текстур, указывая ссылки на верные координаты. При этом левым нижним углом изображения будет (0, 1). Для OpenGL способы подачи данных не важны — программе лишь нужно знать, где искать координаты текстуры для каждой вершины. Способ тоже работает, но ненадежен.
3. Меняем метод “loadTexture[]“, чтобы тот переворачивал изображение при загрузке. Работает, но добавляет время к загрузке приложения. Каждый раз при загрузке текстур, к примеру, для уровня игры, они редактируются центральным или графическим процессором (в зависимости от реализации).
Оптимальным представляется третий вариант — с определяемым спецификой платформы кодом текстур. Возможно, приложение будет обрабатываться не та той платформе, где программировалось, или будет обрабатываться как-то по-другому.
На деле все определяется задачами. В случае с приложением для iPhone, которое позже может оказаться на другой платформе, я бы ограничился переработкой массива координат. Это не повлияет на эффективность загрузки и визуализации, не потребует предварительной обработки в графическом редакторе.
Я буду редактировать метод “loadTexture[]” — для наших уроков это целиком приемлемый вариант. Просто не взваливайте всю работу на OpenGL.
В “loadTexture[]” нам достаточно будет изменить вызов
1 2 3 4 5 6 7 8 9 | CGContextRef textureContext = CGBitmapContextCreate(textureData, texWidth, texHeight, 8, texWidth * 4, CGImageGetColorSpace(textureImage), kCGImageAlphaPremultipliedLast); // Переворачиваем изображение — две новые строки CGContextTranslateCTM(textureContext, 0, texHeight); CGContextScaleCTM(textureContext, 1.0, -1.0); |
Теперь изображения настроены корректно.
Множество текстур из одной
В этом уроке мы попробуем с помощью одной текстуры (т.е. одного вызова “loadTexture[]“) получить четыре отдельных варианта: два для стен плюс для пола и потолка.
В прошлом уроке мы загрузили шесть отдельных текстур — по одной для каждой стороны куба. Каждый раз, загружая текстуру и отправляя ее OpenGL, программа создает для себя копию. Поэтому зарезервированную для изображения текстуры память в методе “loadTexture[]” мы можем освободить (”free()“) с помощью “malloc()“. Само собой, каждое копирование текстуры занимает определенное время, нужно выполнять вызов для привязки OpenGL к новой текстуре каждый раз, когда мы хотим ее изменить и пр.
Такой подход достаточно эффективен, но дело здесь даже не в производительности. С помощью этого удобного навыка я покажу, как задействовать текстуры по частям.
Первым делом я создал новое пустое изображение в Gimp размером 512×512 пикселей (для четырех текстур 256×256). После этого поместил туда четыре текстуры встык. Сохранив все в файл PNG, получил вот такой результат:
Как минимум некоторые (а вообще надеюсь, что все) уже поняли, что через управление координатами текстур я смогу получить доступ к каждой из них в отдельности.
Прежде чем задать координаты текстуры, нужно предварительно указать вершины. Достаточно будет описать один квадрат:
1 2 3 4 5 6 | const GLfloat elementVerticies[] = { -1.0, 1.0, 0.0, // Верхний левый угол -1.0, -1.0, 0.0, // Нижний левый 1.0, -1.0, 0.0, // Нижний правый 1.0, 1.0, 0.0 // Верхний правый }; |
Итак, чтобы визуализировать теперь текстуру дерева (верхний левый квадрат комбинированного изображения), нужно указать следующие координаты:
1 2 3 4 5 6 | cconst GLfloat combinedTextureCoordinate[] = { // Текстура деревянной стены 0.0, 1.0, // Vertex[0~2] верхний левый угол квадрата 0.0, 0.5, // Vertex[3~5] нижний левый угол квадрата 0.5, 0.5, // Vertex[6~8] нижний правый угол квадрата 0.5, 1.0, // Vertex[9~11] верхний правый угол квадрата |
Как и раньше, это всего лишь пары координат. Каждая указывает OpenGL, где в текстуре получать пиксель для конкретной вершины (OpenGL лишь выполняет интерполяцию между вершинами для заполнения).
Поскольку “нижняя часть” деревянной текстуры составляет всего половину полной высоты, координата Y вместо прежнего значения 0.0 принимает 0.5. Если раньше для координат текстур мы использовали данные “Glshort“, теперь для представления дробных величин понадобится “Glfloats“.
Точно так же соответствующая правому краю деревянной текстуры координата X представляет собой лишь половину загруженной текстуры, поэтому составляет 0.5.
Вот полный массив координат для текстуры:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | const GLfloat combinedTextureCoordinate[] = { // Текстура деревянной стены 0.0, 1.0, // Vertex[0~2] верхний левый угол квадрата 0.0, 0.5, // Vertex[3~5] нижний левый угол квадрата 0.5, 0.5, // Vertex[6~8] нижний правый угол квадрата 0.5, 1.0, // Vertex[9~11] верхний правый угол квадрата // Кирпичная текстура 0.5, 1.0, 0.5, 0.5, 1.0, 0.5, 1.0, 1.0, // Текстура пола 0.0, 0.5, 0.0, 0.0, 0.5, 0.0, 0.5, 0.5, // Текстура потолка 0.5, 0.5, 0.5, 0.0, 1.0, 0.0, 1.0, 0.5 }; |
Надеюсь, что теперь — с полным массивом координат — все стало ясно. Массив относится к координатам внутри текстуры, поэтому нет необходимости привязывать их с четырем текстурам, с которых мы начинали.
И последнее. Я выполнил несколько операций “#define“, чтобы упростить считывание кода и получить верное смещение в массиве для координат каждой текстуры:
1 2 3 4 | #define WOOD_TC_OFFSET 0 #define BRICK_TC_OFFSET 8 #define FLOOR_TC_OFFSET 16 #define CEILING_TC_OFFSET 24 |
Рисуем туннель
Возможно, кого-то я сейчас разочарую. Представленный здесь туннель визуализирован не динамически, а статически. Скоро нам предстоит иметь дело с трехмерным пространством и начать перемещаться в туннеле (и в других комнатах), но для этого еще предстоит освоить несколько концепций OpenGL. Пока же нарисуем его простейшим способом.
Визуализированный туннель выглядит так:
Первое, что нужно отметить, — наличие всего одного объекта. Код рисования туннеля не предусматривает создания других предметов, поэтому имеющийся единственный мы заставим выглядеть так, как будто их несколько. Всего объектов 5 на левой стене, 5 — на правой, по 10 для пола и потолка (просматриваются лишь центральные половинки).
Соответственно, думаю, будет совершенно оправданным сказать, что значительная часть данного урока посвящена повторному применению кода.
Прежде чем приступить к рисованию, переведем OpenGL в состояние рисования квадратов с наложенными текстурами. Уже знакомый код будет выглядеть так:
1 2 3 4 5 | glBindTexture(GL_TEXTURE_2D, textures[0]); glVertexPointer(3, GL_FLOAT, 0, elementVerticies); glEnableClientState(GL_VERTEX_ARRAY); glEnable(GL_TEXTURE_2D); glEnableClientState(GL_TEXTURE_COORD_ARRAY); |
Здесь мы сообщаем OpenGL о массиве вершин, предлагаем им воспользоваться, подключаем и активируем наложение текстур. Передать OpenGL информацию о массиве координат текстур невозможно, поскольку она меняется в зависимости от прорисовываемой в данный момент текстуры (пол, потолок, стены и пр.).
Первым делом рисуем пол. Он состоит из двух заданных ранее квадратов, перемещенных в нижнюю половину экрана, повернутых на 90º для имитации пола и нарисованных встык. Код выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // Рисуем пол // Сначала указываем координаты текстур с нужным смещением glTexCoordPointer(2, GL_FLOAT, 0, &combinedTextureCoordinate[FLOOR_TC_OFFSET]); for (int i = 0; i < 5; i++) { glPushMatrix(); { glTraslatef(-1.0, -1.0, -2.0+(i*-2.0)); glRotatef(-90.0, 1.0, 0.0, 0.0); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); } glPopMatrix(); glPushMatrix(); { glTranslatef(1.0, -1.0, -2.0+(i*-2.0)); glRotatef(-90.0, 1.0, 0.0, 0.0); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); } glPopMatrix(); } |
Получили цикл, повторяющий операцию пять раз. При каждом выполнении цикла рисуются два квадрата пола. Отмечу изменения в вызове “glTexCoordPointer()“:
1 2 | glTexCoordPointer(2, GL_FLOAT, 0, &combinedTextureCoordinate[FLOOR_TC_OFFSET]); |
Параметру ссылки на массив нужно передать корректный адрес начальной координаты. Вот почему заданный ранее параметр “#defines” упрощает чтение кода.
Те, кто отслеживает происходящее в Xcode, могут щелкнуть на кнопке “Build & Go“, получив в симуляторе следующий результат:
Остальным придется поверить мне не слово…
Теперь стены. Процесс практически тот же, но только с перемещение вдоль оси Х и вращением по оси Y.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // Рисуем стены // В этот раз меняем массив координат текстур в процессе рисования for (int i = 0; i < 5; i++) { glPushMatrix(); { glTexCoordPointer(2, GL_FLOAT, 0, &combinedTextureCoordinate[BRICK_TC_OFFSET]); glTranslatef(-1.0, 0.0, -2.0+(i*-2.0)); glRotatef(-90.0, 0.0, 1.0, 0.0); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); } glPopMatrix(); glPushMatrix(); { glTexCoordPointer(2, GL_FLOAT, 0, &combinedTextureCoordinate[WOOD_TC_OFFSET]); glTranslatef(1.0, 0.0, -2.0+(i*-2.0)); glRotatef(-90.0, 0.0, 1.0, 0.0); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); } glPopMatrix(); } |
Обратили внимание на вызов для изменения смещения в массиве координат текстур? Все потому, что я для левой и правой стен я выбрал разные текстуры.
Пришло время нарисовать потолок. То же самое, что и с полом, только по оси Y происходит увеличение вместо уменьшения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // Рисуем потолок // Начинаем с настройки указателя координат текстуры glTexCoordPointer(2, GL_FLOAT, 0, &combinedTextureCoordinate[CEILING_TC_OFFSET]); for (int i = 0; i < 5; i++) { glPushMatrix(); { glTranslatef(-1.0, 1.0, -2.0+(i*-2.0)); glRotatef(90.0, 1.0, 0.0, 0.0); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); } glPopMatrix(); glPushMatrix(); { glTranslatef(1.0, 1.0, -2.0+(i*-2.0)); glRotatef(90.0, 1.0, 0.0, 0.0); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); } glPopMatrix(); } |
В итоге имеем готовый туннель.
Согласен — пока ничего особенного, но зато появилось достойное поле для экспериментов. Теперь можем постепенно осваивать темы управления сценами, визуализации пола (или ландшафтов) и стен в пространстве и пр.
Визуализация в текстуру: простой способ
Визуализация в текстуру — процесс создания новой текстуры (результат аналогичен обращению к “loadTexture[]“) из уже визуализированного объекта сцены. Вместо загрузки текстур из bundle’а приложения мы позволяем OpenGL визуализировать сцену, копируем часть ее в буфер и добавляем обратно перед выводом готового результата на экран.
Вдохните поглубже. Все не так сложно, как кажется, и полезнее, чем представляется.
Простой пример визуализации в текстуру — частичное отражение буфера визуализации. Смысл операции появляется при визуализации в буфер готовых эффектов освещения, прозрачности и смешивания. Если их скопировать и использовать в эффекте отражения, для отраженного изображения не придется заново просчитывать освещение, тени и пр.
Вообще-то я не собирался говорить о визуализации в текстуру уже сейчас, но, идя навстречу заданным вопросам, решил рассмотреть простой способ реализации этого эффекта. Результат будет далеко не оптимальным, поскольку мы еще не рассматривали даже освещение, но идея станет ясна.
В конце туннеля мы добавим еще пару квадратов с текстурами и с помощью визуализации в текстуру получим эффект отражения (хоть и оставляющий желать лучшего).
Для начала добавим новые объекты.
В конце туннеля не свет, а новый герой
Первым делом загрузим две новые текстуры. Первая станет фоном, вторая, находящаяся ближе, — с нашим героем Тони Ромо — будет с ней смешиваться. Таким образом, я собираюсь продемонстрировать эффект визуализации в текстуру как минимум со смешиванием (которое мы пока рассмотрели лишь поверхностно, но все же рассмотрели).
Добавим две новые строки к методу “initWithCoder[]“:
1 2 | [self loadTexture:@"bluetex.png" intoLocation:textures[1]]; [self loadTexture:@"romo.png" intoLocation:textures[2]]; |
Загрузив текстуры после отрисовки потолка, создадим имидж для конца туннеля. Для начала настроим “стандартный” массив координат для обеих текстур, потом нарисуем квадрат с непрозрачной текстурой, дополнив полупрозрачным со смешиванием.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | const GLfloat standardTextureCoordinates[] = { 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0 }; // Рисуем синюю текстуру glBindTexture(GL_TEXTURE_2D, textures[1]); glTexCoordPointer(2, GL_FLOAT, 0, standardTextureCoordinates); glPushMatrix(); { glTranslatef(0.0, 0.0, -6.0); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); } glPopMatrix(); glBindTexture(GL_TEXTURE_2D, textures[2]); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glPushMatrix(); { glTranslatef(0.0, 0.0, -5.9); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); } glPopMatrix(); glDisable(GL_BLEND); |
Ввиду неполноценности нашего урока по смешиванию, считаю этот пример намного удачнее. Надеюсь, все помнят, что первым мы рисуем непрозрачный объект, а перед ним — прозрачный. Этот мы и сделали. Фото Томи Ромо я по-быстрому обтравил, чтобы, будучи одного размера с синей текстурой, оно не перекрывало ее полностью.
Приведенный выше код дает следующее изображение:
Кстати, об этом примере со смешиванием. Вернемся к коду и закомментируем “glEnable(GL_BLEND)” (только одну строку). Получим вот такой результат:
Текстурное изображение Ромо скрыло находящийся сзади синий квадрат с текстурой. Смешивание отсутствует. Обращаю на это внимание, чтобы показать, что визуализация в текстуру берет часть уже обработанного изображения. Включая все эффекты.
Откомментируйте строку “glEnable(GL_BLEND)“. Мы оставим этот эффект для визуализации в текстуру. Теперь, когда настройка закончена, проанализируем сам процесс.
Визуализация в текстуру: процесс
Сама визуализация в текстуру достаточно проста (по крайней мере, рассматриваемый сегодня метод). Весь процесс укладывается в 4 этапа:
1. Создаем текстуру визуализации, в которую будем копировать содержимое визуализированного изображения. Выполняется это практически так же, как для текстуры, загружаемой посредством “loadTexture[]“.
2. Визуализируем сцену, получив Тони Ромо в конце туннеля с эффектом смешивания, подтверждающим факт копирования из визуализируемого фрагмента буфера.
3. Копируем в текстуру, настроенную в шаге 1, нужную часть изображения (или его целиком).
4. Визуализируем новую текстуру.
После знакомства с четырьмя шагами стало очевидным, что абсолютно новым будет только третий — все остальные знакомы как минимум частично.
Создаем текстуру визуализации
Изображение, которое планируется использовать в качестве текстуры, в стандартном варианте мы загружаем, форматируем, пересылаем в OpenGL через обращение к “glTexImage2D()“. В случае текстурой визуализации операций меньше — нужно только выделить память в операционной зоне для хранения копируемых из визуализированной сцены пикселей. Это несложно. Обращаемся к “initWithCoder[]” и после загрузки текстур добавляем приведенные ниже строки кода для текстуры визуализации:
1 2 3 4 5 6 | // Настройка буфера визуализации в текстуру glBindTexture(GL_TEXTURE_2D, textures[3]); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 128, 128, 0, GL_RGBA, GL_UNSIGNED_BYTE, nil); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |
Первым делом, как обычно, сообщаем OpenGL, с какой текстурой работает, выбрав id очередной из доступных текстур. Следующей строкой создаем для нее память практически так же, как создавали загружаемую из файла текстуру.
Отличий здесь два. Во-первых, мы жестко задаем размер текстуры в 128×128 пикселей. Мне он показался приемлемым, а вы можете выбрать любой другой (памятуя, что он должен представлять собой степень числа 2).
Последний параметр я установил на ноль (или, если предпочитаете “NULL“). Обычно он содержит буфер с данными по нашему изображению. Поскольку у нас данных по изображению нет, сообщаем об этом OpenGL: программа создаст текстуру самостоятельно, но без занесения данных в выделенную память.
Последними задаем параметры фильтрации — с шагом 1 мы закончили.
Визуализация сцены
Собственно говоря, здесь все ясно. Мы записываем все в буфер визуализации и переходим к шагу 3.
Копируем часть буфера визуализации в текстуру визуализации
Это можно сделать несколькими способами. В данном уроке я рассмотрю только один, который не только самый быстрый, но и гарантированно работает со всеми реализациями (не требует расширений, даже самых распространенных). Еще одно его преимущества — простота реализации.
Мы копируем подраздел буфера визуализации, одновременно захватив все эффекты смешивания (при наличии других захватили бы и их).
В методе “draw view” после кода рисования можно добавить следующие строки кода:
1 2 | glBindTexture(GL_TEXTURE_2D, textures[3]); glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 100, 150, 128, 128); |
Хотите верьте, хотите нет, но вы только что выполнили визуализацию в текстуру. Первая строка понятна — мы сообщаем OpenGL, с какой текстурой будем работать. Реализовать эту непомерно тяжкую задачу нам поможет обращение к “glCopyTexSubImage2D()“. Эта функция берет подраздел текущего буфера визуализации и копирует часть размера и источника в активную текстуру.
В рассмотренном выше случае мы скопировали в текстуру часть оригинала (100, 150) размером 128×128 пикселей. Параметры при этом следующие:
glCopyTexSubImage2D(
target в OpenGL ES всегда GL_TEXTURE_2D
level уровень детализации, рассмотрим в теме по MIP-текстурам
xoffset &
yoffset для целевого объекта, при копировании пикселей, начиная с точки, не совпадающей с (0, 0)
x & y источник (буфер визуализации) координат,
начиная с нижней левой точки (0,0)
width & height Размер копируемого изображения
);
Не забывайте, что информация копируется из двухмерного пространства, поэтому координаты Z отсутствуют. Неприменимы в данном случае и координаты трехмерного пространства — их придется преобразовать обратно в пространство представления. Но это оставим на другой раз — урок и так получается слишком длинным.
Визуализация в текстуру выполнена — теперь можем воспользоваться результатом на свое усмотрение.
Визуализация новой текстуры
Последний шаг объяснений не потребует. Вот код:
1 2 3 4 5 6 7 | glPushMatrix(); { glTranslatef(0.0, -1.0, -2.0); glRotatef(-75.0, 1.0, 0.0, 0.0); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); } glPopMatrix(); |
Немного сместили фрагмент и развернули в пространстве, получив следующий вариант:
И вот наш герой, визуализированный в текстуру. Готово.
Заключение
Урок закончен. Вскоре мы выполним ту же задачу в полноценном трехмерном пространстве.










Июнь 30th, 2009 at 08:11
hi! Большое спасибо за статьи. Если есть возможность - осветите, пожалуйста, две нижеследующие темы:
1. Отображение текста в OpenGL ES
2. Отлавливание события Touch трехмерными объектами в iPhone приложениях. (Как, например, переместить пальцем шар)
Почему-то многие блогеры и туториалы не уделяют внимания этим вопросам(.