Battle of Pirate Bay - не дай пиратам пройти… Firemint эксперементирует с 3G[s]
Июл 02

На днях имел интересный разговор с боссом. Несмотря на то, что профессионально с OpenGL мы оба работаем примерно одинаковое время, он гораздо осведомленнее меня как в этой сфере, так и в графике в целом. Увидев мои уроки, он пошутил: «Если бы я так оценивал OpenGL, никогда бы тебя на работу не взял!».

Причиной его шутки стали мои фразы типа: “OpenGL визуализирует этот как…” или “отправьте OpenGL массив вершин”. Я, как и многие другие читатели этих уроков, прекрасно знаю, что говорить об OpenGL как о чем-то целостном — все равно настаивать на существовании Санта-Клауса. Ни в коем случае не пытаясь ввести кого бы то ни было в заблуждение, я всего лишь упрощаю подачу материала, преподнося его так, как хотел бы, чтобы объясняли мне.

Кстати, мой босс пообещал подобрать хорошую литературу для новичков, поскольку он не считает книги устаревшим артефактом. Если они мне понравятся, обязательно дам вам знать.

Впервые я столкнулся с OpenGL на моей первой работе — в исследовательском отделе государственной организации. Нам утвердили бюджет на ужасно дорогое аппаратное обеспечение от SGI, и в преддверии его прибытия меня еще с одним сотрудником срочно направили на недельные интенсивные курсы изучения OpenGL.

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

Только когда спустя несколько месяцев на моем рабочем столе появился новехонький SGI Indy (поступивший в мое единоличное пользование), я действительно приступил к изучению OpenGL примерно так, как делаю это здесь. Выводил объекты на экран, а после экспериментировал с ними, не обращая внимания на исчезновение перспективы и деформацию. Не задаваясь целью оптимизации ради скорости. Просто анализировал происходящее.

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

Тангенс к нам приходит…

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

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

Сделаем мы это в два этапа. Работа в двухмерном пространстве напоминает поэкранную аркаду типа Defender или ортографическую проекцию, как в игре Syndicate, значительно отличаясь от игры трехмерной. Тем, кому кажется, что в 3D акселерометр важнее касаний, напомню о наличии кнопок огня, паузы и пр. средствам ввода пользовательской информации.

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

Начало — переходим в горизонтальный режим

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

Базового шаблона на этот раз нет, поэтому просто запускаем Xcode и создаем новый проект из шаблона Apple OpenGL ES.

Первым делом настроим устройство для перехода в альбомный режим и избавимся от панели состояния. В окне навигации “Groups & Files” разверните папку “Resources” и откройте относящийся к приложению файл “.plist” (в моем случае это “OpenGLES12-info.plist“, поскольку проект называется “OpenGLES12“).

На экране вы увидите следующее:

Щелкните на последнем объекте, чтобы справа появился значок “+”, а после на этом значке, добавив новый объект. Из раскрывающегося меню под заголовком “Key” выберите “Initial Interface Orientation“. Переключившись на вкладку “Value“, выберите “Landscape (right home button)“.

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

Теперь избавимся от панели состояния, перейдя в полноэкранный режим: еще раз щелкните на кнопке “+”, выберите “Status bar is initially hidden” и установите флажок под полем со значением.

Рисуем объект

Событие касания может происходить и в отсутствие объектов на экране — простым перемещением пальца. Переходим к файлу “EAGLView.m” и методу “drawView[]“. Удалим код рисования квадрата, приведя метод к следующему виду

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)drawView {

[EAGLContext setCurrentContext:context];

glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer);
glViewport(0, 0, backingWidth, backingHeight);

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrthof(-1.0f, 1.0f, -1.5f, 1.5f, -1.0f, 1.0f);
glMatrixMode(GL_MODELVIEW);

glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER_OES];
}

До появления на экране объекта в альбомном режиме осталось два шага. Во-первых, сам код рисования. Во-вторых, редактирование представления для корректного отображения объектов в новом режиме. Сначала создадим код рисования. После, убедившись с помощью кнопки “Build and Go“, что все в порядке, будем переходить к более сложным задачам.

Рисуем мы точку, поэтому после вызова функции “glClear()” и перед сменой буфера добавляем следующий фрагмент:

1
2
3
4
5
6
7
8
9
const GLfloat pointLocation[] = {
0.0, 0.0
};

glPointSize(32.0);
glColor4f(1.0, 1.0, 0.0, 1.0);
glVertexPointer(2, GL_FLOAT, 0, pointLocation);
glEnableClientState(GL_VERTEX_ARRAY);
glDrawArrays(GL_POINTS, 0, 1);

После щелчка на кнопке “Build and Go” симулятор запустится в альбомном режиме, а экран будет выглядеть следующим образом:

Отлично — точку мы нарисовали.

Кофеина хватит?

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

Попробуйте!

Я вижу два способа решения задачи. Проще всего повернуть матрицу проекции на 90º. Альтернативный способ — начать с класса “EAGLView“, но так далеко я еще не забирался и уверен, что этот путь чреват определенными сложностями.

Итак, будем поворачивать матрицу проекции. Вот как это делается:

1
2
3
4
5
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glRotatef(-90.0, 0.0, 0.0, 1.0);
glOrthof(-1.0, 1.0, 1.5, -1.5, -1.0, 1.0);
glTranslatef(-1.0, -1.5, 0.0);

Первая строка кода после “glLoadIdentity()” поворачивает проекцию. Теперь при внесении изменений в координату X редактируется именно она, а не координата Y. Вторая строка — вспомогательная функция для работы с двухмерным пространством. Вместо того чтобы делать центром экрана точку (0, 0) мы смещаем координаты (0, 0) в левую часть экрана (меняем точку отсчета).

Чтобы кнопка возврата была слева, выполните переворот на +90º вместо отрицательного значения.

Мы еще не закончили — нужно проверить на правильность соотношение размеров. Сейчас оно ошибочно, поскольку настройка осуществлялась с расчетом на экран книжной ориентации (высота больше ширины). Попробуем детальнее проанализировать вызов “glOrthof()“:

1
glOrthof(-1.0f, 1.0f, -1.5f, 1.5f, -1.0f, 1.0f);

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

Отсекание определяется каждым передаваемым функции отдельным параметром — слева, справа, вверху, внизу (последние два характеризуют приближенность и удаленность и для двухмерного пространства не имеют значения). Следовательно, объект с координатой X менее -1.0 (т.е. более отрицательной) и более 1.0 на экране не отображается. То же относится и к координатам Y.

Соотношение сторон представляет собой комбинацию пары X и Y. Несложно увидеть, что общая ширина нашего экрана равна двум единицам (abs(-1.0) + abs(1.0) = 2.0), а высота — трем. В альбомном режиме пропорции меняются местами, иначе после визуализации объекты будут выглядеть “сплющенными”.

Чтобы избавиться от двух этих проблем, заменяем “glOrthof()” и последующую функцию “glTranslatef()” представленными ниже:

1
2
glOrthof(-1.5, 1.5, 1.0, -1.0, -1.0, 1.0);
glTranslatef(-1.5, -1.0, 0.0);

Так уже лучше — точка отсчета (0, 0) сдвинулась в левый нижний угол.

Увидев предварительный результат, рассмотрим ту же концепцию подробнее. На самом деле, мы вполне могли обойтись функцией “glTranslatef()“, просто изменив параметры на “glOrthof()“. Итоговый код настройки выглядит следующим образом:

1
2
3
4
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glRotatef(-90.0, 0.0, 0.0, 1.0);
glOrthof(0.0, 3.0, 0.0, 2.0, -1.0, 1.0);

Ширина по-прежнему составляет 3 единицы, а высота 2, но мы указали новые границы обрезки. Обратили внимание на порядок функций? Недопустимо выполнять вращение после вызова “glOrthof()“, поскольку вращение всегда выполняется вокруг (0, 0).

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

Координаты пространства по отношению к экрану

Для начала практическая демонстрация. Переключившись на определение, добавьте новую переменную:

1
GLfloat newLocation[2];

Теперь возвращаемся к реализации и в методе “initWithCoder[]” определяем начальное положение переменной как:

1
2
newLocation[0] = 1.5;
newLocation[1] = 1.0;

Новая “newLocation[0]” — координата X, “newLocation[1]” — координата Y. Где, по вашему мнению, на экране появится точка?

Запомните свой вариант! Давайте добавим еще код, чтобы окончательно прояснить этот момент. В методе “drawView[]” изменим код рисования на:

1
2
3
4
5
6
7
8
glPushMatrix();
glPointSize(32.0);
glColor4f(1.0, 1.0, 0.0, 1.0);
glTranslatef(newLocation[0], newLocation[1], 0.0);
glVertexPointer(2, GL_FLOAT, 0, pointLocation);
glEnableClientState(GL_VERTEX_ARRAY);
glDrawArrays(GL_POINTS, 0, 1);
glPopMatrix();

Помимо смещения текущей матрицы (объекта) мы добавили строку для перемещения точки к координатам X и Y, содержащимися в переменной “newLocation“.

Щелкните на кнопке “Build and Go” и проверьте свою догадку насчет итогового положения точки.

Угадали? Точка снова в центре экрана. Что это говорит нам о соотношении координат пространства и экрана?

Теперь мы знаем, что OpenGL отображает исключительно объекты, которые расположены в пространстве, привязанном к прямоугольнику с исходной точкой (0, 0), шириной в три пункта вправо по оси X и в 2 пункта вверх по оси Y. То есть:

На диаграмме сверху показано то, что выводится на экран iPhone с учетом координат обрезки. Зеленый прямоугольник — отображаемый фрагмент пространства.

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

Обработка касаний

К счастью, ничего сложного нас не ждет. Класс “EAGLView” является подклассом “UIView“, который, в свою очередь, представляет собой подкласс “UIResponder“. Для обработки касаний класс “UIResponder” определяет, но не внедряет следующие четыре метода:

1
2
3
4
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event

touchesBegan[]” запускается при первом касании пользователем экрана;
touchesMoved[]” имеет место при перемещении пальца после “touchesBegan[]“;
touchesEnded[]” запускается, как только пользователь отрывает палец от экрана;

событие “touchesCancelled[]” происходит после “touchesBegan[]“, прерванного системными событиями, например, преследующими меня предупреждениями “network lost“.

Доступа к “UIControlEvents“, включая “TouchUpInside“, у нас нет, поскольку мы не используем “UIControls“.

Для обработки событий в нашем распоряжении вышеперечисленные четыре метода. Для начала рассмотрим “touchesBegan[]” — на его примере я покажу, как превращать координаты экрана в координаты пространства.

Добавьте новый метод к реализации “EAGLView“. Выносить его в заголовочный файл необходимости нет, поскольку он уже определен в суперклассе “UIResponder“.

1
2
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
}

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

1
2
UITouch *t = [[touches allObjects] objectAtIndex:0];
CGPoint touchPos = [t locationInView:t.view];

Желающих узнать, где я взял эту информацию, отправляю к справочным материалам API. Полученные значения я превращу в “проценты” экрана. Так, например, для половины оси я буду получать вместо координаты 0.5 или 50%. Делается это просто:

1
2
3
CGRect bounds = [self bounds];
CGPoint w = CGPointMake((touchPos.x - bounds.origin.x) / bounds.size.width,
(touchPos.y - bounds.origin.y) / bounds.size.height);

Теперь берем представленные “UIView” координаты и

1
2
CGPoint p = CGPointMake((touchPos.x - bounds.origin.x) / bounds.size.width,
(touchPos.y - bounds.origin.y) / bounds.size.height);

Последним шагом преобразуем процентное значение в отображаемые на экране координаты пространства. Для этого нужно переключиться на предоставленные “UITouch” координаты X и Y. Несмотря на осведомленность iPhone об альбомной ориентации, координаты X и Y устройство выдает в книжном варианте. Поэтому созданная выше точка p.x содержит значение Y и наоборот.

1
2
newLocation[0] = 3.0 * p.y;
newLocation[1] = 2.0 * p.x;

Если помните, ширина X отображаемой зоны равнялась трем пунктам. Соответственно, при составляющем 50% от значения X (w.x = 0.5) касании координаты пространства 3.0 * 0.5 = 240.0. То же относится к величине Y.

Добавив представленный выше код, щелкните на кнопке “Build and Go“. Наведите указатель мыши на любое место экрана в симуляторе — и точка последует за ним.

Пока просто помните, что все предоставляемые UI-объектами координаты X необходимо обрабатывать как координаты Y, а Y, соответственно, — как X.

Все это прекрасно, но добиться перемещения за касаниями можно, вставив тот же код в “touchesMoved[]“:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *t = [[touches allObjects] objectAtIndex:0];
CGPoint touchPos = [t locationInView:t.view];

CGRect bounds = [self bounds];

// Берем точку и превращаем ее в "процент" экрана
//   Т.е. 0.85 = 85%
CGPoint p = CGPointMake((touchPos.x - bounds.origin.x) /
bounds.size.width,
(touchPos.y - bounds.origin.y) /
bounds.size.height);

newLocation[0] = 3.0 * p.y;
newLocation[1] = 2.0 * p.x;
}

Получим в точности тот же результат, что с функцией начала касания. Если сейчас щелкнуть на кнопке “Build and Go“, потом на экране симулятора и перетащить по нему указатель, точка последует за щелчками мыши.

ПРИМЕЧАНИЕ: Я заметил, что при перетаскивании влево точка замирает на приличном расстоянии от края и дальше не двигается. При этом симулятор прекращает передавать события касания. Подобные ограничения объясняются исключительно техническими моментами Apple. По возможности позже я проанализирую этот момент подробнее.

Определение координат пальца по отношению к координатам объекта

Я знаю, что не имею права закрывать тему, не рассмотрев вариант с реальным касанием пальцем экрана и промахом. В плоском пространстве все достаточно просто — лишь внесем нужные изменения в метод “touchesBegan[]“.

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

Кроме того, у наших объектов весьма специфичные определения. (1.445, 1.444) — это не то же самое, что (1.444, 1.444), поэтому в определение событий стоит внести толику здравого смысла, иначе можно будет щелкать часами, пытаясь попасть в точную координату точки.

Отредактируем все так, чтобы метод “touchesMoved[]” реагировал только на касание непосредственно к точке, игнорируя остальные объекты на экране. Добавим в определение новую переменную:

1
BOOL fingerOnObject;

и в методе “initWithCoder[]” установим для нее значение “NO“:

1
fingerOnObject = NO;

В методе “touchesBegan[]” удаляем две последние строки там, где присваивали значения переменной “newLocation“, заменив их представленным ниже кодом:

1
2
3
4
5
6
7
8
9
CGRect touchArea = CGRectMake((3.0 * p.y) - 0.1, (2.0 * p.x) - 0.1, 0.2, 0.2);

if ((newLocation[0] > touchArea.origin.x) &&
(newLocation[0] < (touchArea.origin.x + touchArea.size.width))) {
if ((newLocation[1] > touchArea.origin.y) &&
(newLocation[1] < (touchArea.origin.y + touchArea.size.height))) {
fingerOnObject = YES;
}
}

Здесь я создал прямоугольник, по площади превышающий зону касания за счет смещения координат X и Y на 0,1 пункта при ширине и высоте 0,2 пункта. Обратите внимание: ширина и высота вдвое больше величины смещения X и Y. Все операторы “if” проверяют, находится ли точка внутри этой зоны касания.

После этого в самом начале метода “touchesMoved[]” добавляем следующий оператор “if“:

1
2
3
if (!fingerOnObject) {
return;
}

Если переменная “fingerOnObject” не установлена на “YES“, перемещение пальца по экрану значения не имеет. Последним шагом внедряем метод “touchesEnded[]” для сброса переменной “fingerOnObject“.

Вот и все. Щелкаем на кнопке “Build and Go“, потом на точке — двигаться она будет только при попадании в зону касания.

При первом перемещении могут наблюдаться частичные скачки, зависящие от места щелчка: все в порядке, просто связь с пальцем пользователя может частично утрачиваться. Можно частично уменьшить зону касания или (как сделал бы я) сместить наполовину в методе “touchesBegan[]” и доверить вторую полосину первому обращению к “touchesMoved[]“. Учитывая палец, этого будет достаточно для покрытия начального скачка.

Обнаружение касаний к квадрату

Коснуться квадрата проще, нежели точки. Местонахождение квадрата известно благодаря переменной “newLocation“. Знаем смещение для массива вершин квадрата, ширину и высоту. Следовательно, все, что нужно, — обычное сопоставление.

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

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

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

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

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


6 Responses to “Уроки iPhone SDK: (Часть 12) OpenGL ES: Альбомный режим и обработка событий: двухмерное пространство (Часть 1)”

  1. 1. iБратан Says:

    Продолжай в том же духе! /me Подписался на RSS и жду новых учебных статей!

  2. 2. Asver Says:

    Если можешь - выложи названия хороших книг для начинающих.

  3. 3. Artem Says:

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

  4. 4. Batu Says:

    Artem
    А вы можете книгу перевести?Пусть будет долго, зато поможет многим.Если да, то пишите на мыло или в асю)

  5. 5. Al Says:

    Исходники пропали, автор удалил их. Можете перезалить?

  6. 6. Artem Says:

    На сайте автора этого и других уроков из этой серии какие-то неполадки. Он не доступен. Как толтько сайт поднимется все станет доступно. Если поднимется конечно :)

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