Pull to refresh

OpenGL ES: Перемещение в 3D

Reading time9 min
Views33K
Original author: Simon Maurice
В Сидней практически пришла зима — и я умудрился подхватить грипп (простой, не свиной). А тут еще работа, день Матери и пр. и пр. Одним словом, ввиду недостатка времени двигаться будем быстро. Но прежде чем приступать к созданию «уникального» трехмерного мира, освоим концепции перемещения в 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()", вы обязательно оцените ее простоту и удобство. Посмотрим на прототип:

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" — прямо сейчас!

Исходный код к уроку можно скачать здесь.
Tags:
Hubs:
Total votes 40: ↑33 and ↓7+26
Comments21

Articles