Rolando 2 уже в App Store Неужели легендарная GTA для iPhone?
Июл 03

В Сидней практически пришла зима — и я умудрился подхватить грипп (простой, не свиной). А тут еще работа, день Матери и пр. и пр. Одним словом, ввиду недостатка времени двигаться будем быстро. Но прежде чем приступать к созданию “уникального” трехмерного мира, освоим концепции перемещения в 3D пространстве.

Нам предстоит освоить код обработки событий, который позволит ходить “по полу”. С помощью касаний мы будем поворачивать влево, вправо, перемещаться вперед и назад. Обойдемся без бега, поворотов головы и наведения резкости, хотя добавить их легко. Подобные ограничения объясняются как желанием упростить изложение, так и возможностью для не располагающих 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()“, вы обязательно оцените ее простоту и удобство. Посмотрим на прототип:

1
2
3
4
5
6
7
8
9
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()” добавьте приведенный ниже код:

1
2
3
4
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”.

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

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

1
2
GLfloat eye[3];// Откуда мы смотрим
GLfloat center[3];// Куда мы смотрим

Названия “eye” и “center” при желании вполне можно заменить на “position” и “facing” — существенного значения это не имеет (я просто использовал термины функции “gluLookAt()“).

Две переменные содержат координаты X, Y и Z. Величину Y можно жестко прописать в коде, т.к. она не меняется, но я решил обойтись без лишних движений.

Переходим к методу “initWithCoder:“. Здесь инициализируем две переменные со значениями, использованными ранее для обращения к “gluLookAt()“:

1
2
3
4
5
6
7
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()” измените на:

1
2
gluLookAt(eye[0], eye[1], eye[2], center[0], center[1], center[2],
0.0, 1.0, 0.0);

Для полного спокойствия щелкните на кнопке “Build & Go“, убедившись, что все работает.

Готовимся к перемещению

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

Для начала определимся со скоростью ходьбы и поворотов:

1
2
#define WALK_SPEED 0.005
#define TURN_SPEED 0.01

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

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

1
2
3
4
5
6
7
typedef enum __MOVMENT_TYPE {
MTNone = 0,
MTWalkForward,
MTWAlkBackward,
MTTurnLeft,
MTTurnRight
} MovementType;

Теперь в процессе функционирования приложения мы можем стоять (MTNone), идти вперед, назад, поворачиваться влево и вправо. Боюсь, что этим пока нам придется и ограничиться.

Осталось указать переменную, содержащую текущее движение:

1
MovementType currentMovement;

Не забудьте перейти к методу “initWithCoder:” и задать значение по умолчанию для переменной “currentMovement“:

1
currentMovement = MTNone;

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

Коснись меня

Разобравшись с основами, можно переходить собственно к обработке касаний. Если помните, в прошлом уроке я представил все четыре метода их обработки. На этот раз — для простоты — мы воспользуемся только двумя: “touchesBegan” и “touchesEnded“.

Чтобы определить предпринимаемое действие, экран iPhone я разделил на четыре зоны:

Стандартная высота экрана — 480 пикселей. Делим его на 3 равные части по 160 пикселей. Пиксели 0~160 соответствуют движению вперед, 320~480 — перемещению назад, центральные 160 поделены на правую и левую половины для поворотов.

Вот теперь можно представить первый из методов касания:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
- (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“.

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

currentMovement = MTNone;
}

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

1
- (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()“. Начнем с метода реализации и парочки базовых принципов:

1
2
3
4
5
6
- (void)handleTouches {

if (currentMovement == MTNone) {
// Мы идем в никуда, и делать там нечего
return;
}

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

Независимо от того, движемся мы или поворачиваемся, необходимо знать значения “deltaX” и “deltaZ“. Сохраняю их в вызываемом переменной векторе:

1
2
3
4
5
GLfloat vector[3];

vector[0] = center[0] - eye[0];
vector[1] = center[1] - eye[1];
vector[2] = center[2] - eye[2];

Я рассчитал и значение Y delta, хотя нам оно не нужно.

Теперь выясняем, какие действия по перемещению предпринимать. Все содержится в операторе выбора:

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
26
27
28
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():” добавьте следующую строку:

1
2
[self handleTouches];
[self handleTouches];

Все готово!

Можно щелкать на кнопке “Build and Go” — прямо сейчас!

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

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

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

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

1 звезда2 звезд3 звезд4 звезд5 звезд (Оцените приложение)
Загрузка ... Загрузка ...


3 Responses to “Уроки iPhone SDK: (Часть 13) OpenGL ES: Перемещение в 3D”

  1. 1. loxx45 Says:

    а еще будут статьи про opengl??

  2. 2. Artem Says:

    Завтра не обещаю но мы планируем. Сейчас есть 15 уроков по OpenGL,если что пользуйтесь содержанием тут.

  3. 3. Artem Says:

    Завтра не обещаю но мы планируем. Сейчас есть 15 уроков по OpenGL, если что пользуйтесь содержанием тут.

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