|
Июл
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” — прямо сейчас!
До свободы перемещений еще далеко, но с учетом кода результат вполне достойный. Работы осталось не так уж и много, а с принципами вы уже знакомы.



Сентябрь 12th, 2009 at 10:33
а еще будут статьи про opengl??
Сентябрь 12th, 2009 at 11:52
Завтра не обещаю но мы планируем. Сейчас есть 15 уроков по OpenGL,если что пользуйтесь содержанием тут.
Сентябрь 12th, 2009 at 11:52
Завтра не обещаю но мы планируем. Сейчас есть 15 уроков по OpenGL, если что пользуйтесь содержанием тут.