
Нам предстоит освоить код обработки событий, который позволит ходить «по полу». С помощью касаний мы будем поворачивать влево, вправо, перемещаться вперед и назад. Обойдемся без бега, поворотов головы и наведения резкости, хотя добавить их легко. Подобные ограничения объясняются как желанием упростить изложение, так и возможностью для не располагающих iPod Touch или iPhone добиваться аналогичных результатов в симуляторе.
Для начала загрузим основу проекта здесь.
Кода там не много — в основном объяснения, что и как происходит.
Мифическая камера
Большинство воспринимает 3D миры как пространство, на которое смотришь через камеру, но в OpenGL камеры как таковой нет. Для иллюзии движения по сцене относительно начальной точки (0, 0, 0) перемещаются объекты, а не камера, как в кино.
Процесс может показаться трудоемким, но это не так. В зависимости от приложения есть множество способов решения данной задачи и еще больше — оптимизации для действительно больших миров. На этом я вкратце остановлюсь чуть позже.
Чтобы немного упростить работу, к уроку я приложил удобную игрушку от «большого брата» OpenGL ES — библиотеки GLU: я имею в виду функцию "gluLookAt()".
Хотя в этих статьях я редко упоминаю OpenGL, думаю, что с библиотекой GLU знакомы практически все. К сожалению, она не входит в спецификации OpenGL ES, но это не означает, что мы не сможем воспользоваться полезными нам функциями. Для работы с ними не обязательно переносить всю библиотеку — выберите лишь актуальные для вас опции.
Функцию "gluLookAt()" я взял из релиза SGI Open Source. Выбор объясняется исключительно тем, что она оказалась под рукой, а я знаком с принципами ее работы. Лицензия на функцию находится здесь же в коде (автором кода являюсь не я). Для тех, кого этот вариант по тем или иным причинам не устраивает, есть масса альтернатив из открытых источников.
Если решите работать с другим кодом или импортировать иные функции, не забудьте поменять все "GLdouble" на "GLfloat", а все привязанные к gl вызовы на версии с плавающей запятой. Еще одна общая рекомендация — избегайте всего, что ориентировано на пользовательский интерфейс (функции ввода, окна). В целом, моментов, на которые нужно обращать внимание, масса, но остальные достаточно очевидны.
Для профессиональных целей ищите последние обновления бесплатных версий. Замечу, что Mesa не рекомендуют сами создатели — она не обновляется, активная разработка приостановлена. Я знаю, что в Интернет есть код для iPhone на базе Mesa GLU, но для профессионального применения он не подходит (читай: содержит ошибки).
Если кому-то интересно, почему разработчики вместо своей библиотеки рекомендуют SGI или другие решения, поищите информацию на сайте Mesa.
Работа с «gluLookAt()»
Освоив функцию "gluLookAt()", вы обязательно оцените ее простоту и удобство. Посмотрим на прототип:
void gluLookAt( GLfloat eyex,
GLfloat eyey,
GLfloat eyez,
GLfloat centerx,
GLfloat centery,
GLfloat centerz,
GLfloat upx,
GLfloat upy,
GLfloat upz)
Согласен, 9 параметров временами многовато, но здесь главное — разобраться. Первые три характеризуют позицию зрителя (это просто координаты X, Y, Z).
Вторые три относятся к рассматриваемому объекту (вновь трио X, Y, Z).
Последние три можно объединить в вектор «вверх». Сейчас мы их рассматривать не будем, поскольку нужный эффект дают именно две первые позиции.
Координаты зрителя (глаз) — это и есть мифическая камера. Естественно, они соотносятся с координатами пространства. Фактически, это точка в пространстве, откуда вы наблюдаете за происходящим. Координаты «center» соответствуют направлению взгляда, т.е. его цели. Если координата "center" Y находится выше координаты взгляда Y, пользователь смотрит вверх. Если меньше, то, соответственно, — вниз.
Наш базовый проект уже настроен, но без перемещений. Мы нарисовали пол и смотрим в никуда:

Вот что получится при щелчке на кнопке "Build and Go".
Для начала попробуем поработать с функцией "glLookAt()". Перейдите к методу "drawView:" и после вызова "glLoadIdentity()" добавьте приведенный ниже код:
glLoadIdentity();
gluLookAt(5.0, 1.5, 2.0, // Положение глаз, взгляд "из"
-5.0, 1.5, -10.0, // Цель, взгляд "на"
0.0, 1.0, 0.0); // Пока игнорируем
Еще раз щелкните на кнопке "Build and Go", с удовольствием убедившись, что все работает. Результат в симуляторе должен быть следующим:

Единственным обращением к функции мы перевели взгляд из одного угла в противоположный. Поэкспериментируйте с параметрами "glLookAt()", наблюдая за происходящим.
Перемещение в 3D
Теперь, получив представление о "gluLookAt()", предлагаю воспроизвести прогулку по полу. В действительности двигаться мы будем вдоль двух осей (X и Z, т.е. без изменения высоты), меняя направление с помощью поворота.
Если вспомнить функцию "gluLookAt()", какая информация, по вашему мнению, нужна для прогулок в трехмерном пространстве?
Понадобятся:
локация зрителя «eye»;
направление взгляда (цель) «centre».
Зная две эти вводные, мы готовы обрабатывать информацию от пользователя, позволяя ему контролировать местонахождение в пространстве.
Предположим, мы решили начать с двух задействованных ранее величин. Пока двигаться не позволяет жестко закодированная информация фрагмента, поэтому для начала перейдем к интерфейсу и добавим следующие переменные:
GLfloat eye[3];// Откуда мы смотрим
GLfloat center[3];// Куда мы смотрим
Названия "eye" и "center" при желании вполне можно заменить на "position" и "facing" — существенного значения это не имеет (я просто использовал термины функции "gluLookAt()").
Две переменные содержат координаты X, Y и Z. Величину Y можно жестко прописать в коде, т.к. она не меняется, но я решил обойтись без лишних движений.
Переходим к методу "initWithCoder:". Здесь инициализируем две переменные со значениями, использованными ранее для обращения к "gluLookAt()":
eye[0] = 5.0;
eye[1] = 1.5;
eye[2] = 2.0;
center[0] = -5.0;
center[1] = 1.5;
center[2] = -10.0;
Возвращаемся к методу "drawView:". Вызов "gluLookAt()" измените на:
gluLookAt(eye[0], eye[1], eye[2], center[0], center[1], center[2],
0.0, 1.0, 0.0);
Для полного спокойствия щелкните на кнопке "Build & Go", убедившись, что все работает.
Готовимся к перемещению
Прежде чем мы сможем обрабатывать события, перемещаясь в пространстве, необходимо настроить ряд моментов в заголовочном файле. Переключитесь на него, чтобы задать несколько настроек по умолчанию и создать новый тип перечня.
Для начала определимся со скоростью ходьбы и поворотов:
#define WALK_SPEED 0.005
#define TURN_SPEED 0.01
Мне эти значения представляются несколько замедленными, поэтому, разобравшись с их работой, можете внести свои собственные.
Следующим шагом создаем перечислимый тип, чтобы в точности сохранять наши действия. Добавим следующее:
typedef enum __MOVMENT_TYPE {
MTNone = 0,
MTWalkForward,
MTWAlkBackward,
MTTurnLeft,
MTTurnRight
} MovementType;
Теперь в процессе функционирования приложения мы можем стоять (MTNone), идти вперед, назад, поворачиваться влево и вправо. Боюсь, что этим пока нам придется и ограничиться.
Осталось указать переменную, содержащую текущее движение:
MovementType currentMovement;
Не забудьте перейти к методу "initWithCoder:" и задать значение по умолчанию для переменной "currentMovement":
currentMovement = MTNone;
По умолчанию это значение для переменной будет таковым в любом случае, но подобные действия — хорошая практика.
Коснись меня
Разобравшись с основами, можно переходить собственно к обработке касаний. Если помните, в прошлом уроке я представил все четыре метода их обработки. На этот раз — для простоты — мы воспользуемся только двумя: "touchesBegan" и "touchesEnded".
Чтобы определить предпринимаемое действие, экран iPhone я разделил на четыре зоны:

Стандартная высота экрана — 480 пикселей. Делим его на 3 равные части по 160 пикселей. Пиксели 0~160 соответствуют движению вперед, 320~480 — перемещению назад, центральные 160 поделены на правую и левую половины для поворотов.
Вот теперь можно представить первый из методов касания:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *t = [[touches allObjects] objectAtIndex:0];
CGPoint touchPos = [t locationInView:t.view];
// Определяем позицию на экране. Нас интересуют исключительно
// координаты экрана iPhone, а не пространства
// поскольку мы всего лишь обрабатываем события.
//
// (0, 0)
// +-----------+
// | |
// | 160 |
// |-----------| 160
// | | |
// | | |
// |-----------| 320
// | |
// | |
// +-----------+ (320, 480)
//
if (touchPos.y < 160) {
// Идем вперед
currentMovement = MTWalkForward;
} else if (touchPos.y > 320) {
// Идем назад
currentMovement = MTWAlkBackward;
} else if (touchPos.x < 160) {
// Поворачиваем налево
currentMovement = MTTurnLeft;
} else {
// Поворачиваем направо
currentMovement = MTTurnRight;
}
}
При касании пользователем экрана останется зафиксировать сегмент и указать переменную, чтобы знать, что делать, когда настанет момент расчета нового положения. Не забывайте, что выносить определение метода в интерфейс необходимости нет — такие методы наследуются.
Настала очередь метода "touchesEnded".
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
currentMovement = MTNone;
}
Собственно говоря, все понятно. Теперь нам нужен метод для обработки данных событий касания. На этот раз потребуется декларация метода в интерфейсе. Переключаемся на заголовочный файл и добавляем следующее определение метода:
- (void)handleTouches;
Переходим обратно и приступаем к его реализации. В этом методе мы будем рассчитывать перемещение по трехмерному пространству.
Теория перемещений в 3D
Начнем с базовых понятий. Уверен, никто не удивиться, когда узнает, что это лишь один из способов расчета новых локаций в трехмерном пространстве при n-ном числе перемещений вдоль любого вектора v. К сожалению, не помню, кто первым это сказал (возможно, Arvo). В любом случае, это было давно — еще до того, как Wolf 3D показал, как это происходит в реальном времени.
Первой рассмотрим ходьбу. Если пользователь сообщает о желании идти вперед, нужно не только учитывать вид из локации зрителя, но и не упускать целевой точки. Взгляд из локации сообщает нам о текущем местонахождении, а взгляд на цель определяет направление движения.
Любая картинка лучше тысячи слов: взгляните на изображение, представляющее взгляд из точки и взгляд на цель.

При подобном методе перемещения расстояние между двумя точками является величиной дельта для координат X и дельта для координат Y. Осталось получить новые значения X и Z умножением текущих координат на величину «скорости». Примерно так:

Мы легко рассчитаем новые координаты для красной точки.
Начинаем с deltaX and deltaZ:
deltaX = 1.5 — 1.0 = 0.5
deltaZ = -10 — (- 5.0) = -5.0
Умножаем на скорость ходьбы:
xDisplacement = deltaX * WALK_SPEED
= 0.5 * 0.01
= 0.005
zDisplacement = deltaZ * WALK_SPEED
= -5.0 * 0.01
= 0.05
Соответственно, новая координата, представленная на рисунке выше красной точкой:eyeC + CDisplacement
(eyex + xDisplacement, eyey, eyez + zDisplacement)
= (0.005+1.0, eyey,(-10)+ 0.05)
= (1.005, eyey, -9.95)
Замечу, что предложенный метод не лишен недостатков. Основная проблема в том, что чем больше расстояние между локацией зрителя и объектом взгляда, чем выше «скорость ходьбы». Тем не менее, вопрос решаем, а с точки зрения ресурсов CPU менее затратен по сравнению со многими прочими алгоритмами движения.
Размеры нашего мирка невелики, а вот на деле разница между зрителем и объектом взгляда окажется слишком огромной, поэтому обязательно поэкспериментируйте. Как выяснится, скорость перемещения непосредственно зависит от соотношения расстояния между двумя точками и величины "WALK_SPEED".
Осталось рассмотреть повороты влево/вправо.
Зачастую мне приходится сталкиваться с кодом, в котором программисты ответственно выписывают угол, под которым визуализируется сцена. Это не наш случай. Рабочий угол нам известен, поскольку известны две точки (вспомните Пифагора — у нас правильный треугольник).
Взгляните на рисунок:

Чтобы инициировать поворот, нам нужно всего лишь переместить по кругу взгляд на целевой объект. Наше определение "TURN_SPEED", фактически, является углом поворота.
Ключ к происходящему: нет необходимости корректировать координаты зрителя — меняется объект взгляда. Откладывая на виртуальной окружности перед глазами новую точку-локацию (т.е. постепенно увеличивая значение угла, определяемое "TURN_SPEED"), получаем новый «угол поворота».
Раз поворот соответствует нарисованному кругу, центральной точкой которого является локация зрителя или точка взгляда, достаточно вспомнить принципы рисования круга.
Другими словами, все сводится к:
newX = eyeX + radius * cos(TURN_SPEED)*deltaX — sin(TURN_SPEED)*deltaZ
newZ = eyeZ + radius * sin(TURN_SPEED)* deltaX +
cos(TURN_SPEED)*deltaZ
Обработка событий с преобразованием в движения
Опробуем изложенное на практике.
Вернувшись к реализации, оттолкнемся от касаний, чтобы получить новые параметры для "gluLookAt()". Начнем с метода реализации и парочки базовых принципов:
- (void)handleTouches {
if (currentMovement == MTNone) {
// Мы идем в никуда, и делать там нечего
return;
}
Для начала проверяем факт перемещения. Если он отсутствует, делать больше нечего.
Независимо от того, движемся мы или поворачиваемся, необходимо знать значения "deltaX" и "deltaZ". Сохраняю их в вызываемом переменной векторе:
GLfloat vector[3];
vector[0] = center[0] - eye[0];
vector[1] = center[1] - eye[1];
vector[2] = center[2] - eye[2];
Я рассчитал и значение Y delta, хотя нам оно не нужно.
Теперь выясняем, какие действия по перемещению предпринимать. Все содержится в операторе выбора:
switch (currentMovement) {
case MTWalkForward:
eye[0] += vector[0] * WALK_SPEED;
eye[2] += vector[2] * WALK_SPEED;
center[0] += vector[0] * WALK_SPEED;
center[2] += vector[2] * WALK_SPEED;
break;
case MTWAlkBackward:
eye[0] -= vector[0] * WALK_SPEED;
eye[2] -= vector[2] * WALK_SPEED;
center[0] -= vector[0] * WALK_SPEED;
center[2] -= vector[2] * WALK_SPEED;
break;
case MTTurnLeft:
center[0] = eye[0] + cos(-TURN_SPEED)*vector[0] -
sin(-TURN_SPEED)*vector[2];
center[2] = eye[2] + sin(-TURN_SPEED)*vector[0] +
cos(-TURN_SPEED)*vector[2];
break;
case MTTurnRight:
center[0] = eye[0] + cos(TURN_SPEED)*vector[0] - sin(TURN_SPEED)*vector[2];
center[2] = eye[2] + sin(TURN_SPEED)*vector[0] + cos(TURN_SPEED)*vector[2];
break;
}
}
Вот и весь метод обработки касаний. Реализация представляет собой уже рассмотренный нами ранее алгоритм.
Сводим воедино
Вернитесь к методу "drawView" и перед вызовом "gluLookAt():" добавьте следующую строку:
[self handleTouches];
[self handleTouches];
Все готово!
Можно щелкать на кнопке "Build and Go" — прямо сейчас!
Исходный код к уроку можно скачать здесь.