Д. Конгер. Физика для разработчиков компьютерных игр
Часть I. Физика, математика и программирование игр
Что я должен знать из математики, чтобы писать игры?
Что я должен знать из программирования?
Глава 2. Имитация 3D-графики с помощью DirectX
Два представления DirectX
Использование DirectX
Глава 3. Математические инструменты
Двумерные системы координат
Трехмерные и четырехмерные системы координат
Единицы измерения
Векторы
Матрицы
Глава 4. 2D-преобразования и рендеринг
Применение преобразований - вращающийся треугольник
Глава 5. 3D-преобразования и рендеринг
3D-конвейер
Рендеринг в 3D
Глава 6. Сетчатые модели и X-файлы
Материалы
Загрузка сетчатой модели
Класс d3d_mesh
Часть II. 3D-объекты, движение и столкновения
Одномерная кинематика
Силы
Двумерная и трехмерная кинематика
Моделирование материальных точек
Материальные точки в играх
Глава 8. Столкновения материальных точек
Реакция на столкновения
Глава 9. Динамика твердых тел
Центр масс
Вращение двумерных твердых тел
Твердые тела в 3D
Ориентация
Реализация твердых тел в 3D
Глава 10. Столкновения твердых тел
Реакция на столкновения
Глава 11. Сила тяжести и метательные снаряды
Траектории метательных снарядов
Моделирование движения метательных снарядов
Глава 12. Система масс м пружин
Основы: гармонические колебания
Закон Гука
Затухающие гармонические колебания
Реализация ткани
Глава 13. Вода и волны
Сопротивление движению
Течение в  воде
Волны
Вода в программах
Объекты в воде
Часть III. Практические примеры
Введение в DirectInput
Перемещение камеры в DirectX
Глава 15. Автомобили, корабли и лодки
Транспорт на воздушной подушке и антигравитационные транспортные средства
Корабли и лодки
Глава 16. Авиация и космические корабли
Физика самолетов
Физика космических кораблей
Эпилог
Часть IV. Приложения
Приложение B. Краткий обзор языка C++
Классы и объектно-ориентированное программирование
Пространства имен
Наследование
Исключения
Другие способы создания новых типов
Приложение C. Основы программирования в Windows
Текст
                    ПРОГРАММИСТУ
йо йэкщр
ФИЗИКА


PHYSICS MODELING FOR GAME PROGRAMMERS David Conger THOMSON COURSE TECHNOLOGY Professional ■ Trade ■ Reference
ПРОГРАММИСТУ Д. Конгер ФИЗИКА ДЛЯ РАЗРАБОТЧИКОВ КОМПЬЮТЕРНЫХ ИГР Перевод с английского А. С. Молявко Др. Москва БИНОМ. Лаборатория знаний 2007
К64 Конгер Д. К64 Физика для разработчиков компьютерных игр / Д. Конгер; Пер. с англ. А. С. Молявко. — М.: БИНОМ. Лаборатория знаний, 2007. — 520 с: ил. ISBN 5-94774-317-5 (русск.) ISBN 1-59200-093-2 (англ.) Рассматриваются вопросы физического моделирования окружающего мира при разработке компьютерных игр. Кроме собственно физики в книге приводятся примеры практического применения физических моделей в играх. Описание простой платформы физического моделирования затем переходит в плоскость изложения принципов моделирования отдельных физических явлений, применимых к играм. Рассматриваются вопросы программирования приложений с использованием созданных инструментов. Представленные в книге модели написаны на C++ с применением DirectX и компилировались в VS.NET. К книге прилагается компакт-диск, содержащий все примеры и необходимый инструментарий. Для чтения книги достаточно знания физики и математики в пределах школьного курса и первичного опыта программирования на C++. Для программистов компьютерных игр, студентов и старшеклассников, интересующихся программированием. УДК 530.1+004.7 ББК 32.973.202 Учебное издание Конгер Дэвид Физика для разработчиков компьютерных игр Ведущий редактор А С. Молявко Художник Ф. Инфантэ Художественный редактор О- Лапко Компьютерная верстка Л. П. Черепанова, Л. В. Катуркина Подписано в печать 26.09.06. Формат 70 х 100%g Гарнитура Школьная. Усл. печ. л. 42,25. Тираж 1500 экз. Заказ 5381 Издательство «БИНОМ. Лаборатория знаний» Адрес для переписки: 125167, Москва, проезд Аэропорта, 3 Телефон: D95I57-5272. E-mail: Lbz@aha.ru http://www.Lbz.ru При участии ООО «ПФ «Сатко» Отпечатано в ОАО «ИПК «Ульяновским Дом печати» 432980,1 Ульяновск, ул 1опчарова, 14 © 2004 by Thomson Course Technology PTR. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording, or by any information storage or retrieval system without written permission from Thomson Course Technology PTR, except for the inclusion of brief quotations in a review. © Перевод на русский язык, оформление, «БИНОМ. Лаборатория знаний», 2007 ISBN 5-94774-317-5 (русск.)
Краткое оглавление Введение 8 Часть I. Физика, математика и программирование игр 11 Глава 1. Физика в играх 12 Глава 2. Имитация ЗР-графики с помощью DirectX 19 Глава 3. Математические инструменты 51 Глава 4. 2Р-преобразования и рендеринг 96 Глава 5. ЗР-преобразования и рендеринг 119 Глава 6. Сетчатые модели и Х-файлы 138 Часть II. ЗР-объекты, движение и столкновения 159 Глава 7. Динамика материальных точек 160 Глава 8. Столкновения материальных точек 187 Глава 9. Динамика твердых тел 217 Глава 10. Столкновения твердых тел 249 Глава 11. Сила тяжести и метательные снаряды 282 Глава 12. Системы масс и пружин 310 Глава 13. Вода и волны 345 Часть III. Практические примеры 375 Глава 14. Готовимся создавать игры 376 Глава 15. Автомобили, корабли и лодки 415 Глава 16. Авиация и космические корабли 440 Эпилог 470 Часть IV. Приложения 471 Приложение А. Глоссарий 472 Приложение В. Краткий обзор языка C++ 475 Приложение С. Основы программирования для Windows 489 Предметный указатель 498 Оглавление 515
Моим детям, наполнившим скучную повседневность смыслом... Благодарности Я хотел бы поблагодарить множество талантливых людей, работа которых сделала возможным издание этой книги. Более всех прочих я благодарен профессору Рассу Хайгли, физику, за его помощь и моральную поддержку. Кроме того, я хочу поблагодарить астронома, физика, писателя и редактора Дэвида Дженнера. Я очень ценю свое знакомство с этими двумя людьми. Далее, моя искренняя благодарность людям, поддерживавшим меня при написании этой книги, прежде всего Митци Коонтц и Карен Джилл. И наконец, благодарю мою семью за терпеливое ожидание моего возвращения из офиса, в котором я работал над этой книгой. Об авторе Дэвид Конгер (David Conger) занимается программированием более 20 лет. С точки зрения Интернета - это, наверное, что-то около 300 лет. Написав целиком очень много программ (включая программы управления графическими контроллерами для военной авиации, игры для DOS и многопользовательские Интернет-игры, а также великое множество коммерческих приложений), он решил стать писателем. Несмотря на противодействие студентов, он ушел из колледжа, в котором читал курсы по компьютерным наукам и коммерческим компьютерным системам. В течение почти семи лет он занимался написанием документации для Microsoft. Он готовил документацию к Xbox Development Kit (XDK), DirectDraw и Direct3D (версий 5 и 6), OpenGL, Extensible Scene Graph (XSG), Image Color Management (ICM), Still Image (STI), Windows Image Acquisition (WIA), Remote Procedure Calls (RPC), компилятору Microsoft Interface Definition Language (MIDL) и Mobile Internet Toolkit (MIT). Его первой книгой, вышедшей в 1987 году, был сборник сказок Индии и стран Дальнего Востока. С тех пор он работал над книгами по С, C++, С# и .NET, а также над учебниками по микрокомпьютерам. Сейчас Дэвид обитает в глуши на западе штата Вашингтон. Там он продолжает мечтать о втором путешествии на восток, гуляя по дорогам с Биг- лем, своей огромной собакой (если хотите, можете считать ее маленькой мохнатой лошадкой). Кроме Бигля, семью Дэвида составляют выводок прекрасных детей и жена - женщина поистине выдающихся достоинств.
Об авторе 7 О редакторе Андре Ламотте, СЕО компании Xtreme Games LLC и создатель XGameSta- tion, работает с компьютерами более 27 лет. Он написал свою первую игру для компьютера TRS-80 и с тех самых пор не прекращает работы над играми. Он также работал с двумерной и трехмерной графикой, занимался исследованиями в области искусственного интеллекта в NASA, создавал компиляторы, участвовал в проектировании роботов, систем виртуальной реальности и коммуникационных систем. Его книги - бестселлеры по программированию игр, и его опыт отражен в книгах серии «Game Development» издательства Thompson Course Technology PTR. С ним можно связаться по адресу ceo@nurve.net и через сайт www.xgamestation.com. От редактора Эта книга - первая в серии о разработке игр, посвященная моделированию физики в играх. Как всегда, я не хотел готовить узко специализированную книгу, посвященную только одному аспекту создания игр; вместо этого я предпочел книгу, очерчивающую общую область, излагающую основы и указывающую направление дальнейшего развития. Большая часть книг по моделированию физики в играх подробно рассматривают именно физику, но в них не хватает примеров практических реализаций и применений изложенного в играх. Книга начинается с описания простой платформы физического моделирования, а затем переходит к методикам моделирования отдельных физических явлений, применимых к играм - например, моделированию твердых тел, материальных точек, столкновений объектов и так далее. Вооружившись инструментами для моделирования этих предметов и явлений, мы сможем приступить к более сложным вопросам расчетов траекторий движения, силы тяжести, пружин и динамики жидкостей. После этого в книге рассматриваются вопросы программирования приложений с использованием созданных инструментов - например, программирование поведения наземных и воздушных транспортных средств. Именно это, на мой взгляд, самая ценная часть книги. Прочтя ее, вы убедитесь, что в ваших силах смоделировать физику в авто- и даже авиасимуляторах. На рынке есть ряд книг по моделированию физики в играх, но ни одна из них не описывает применение физики в играх так подробно, как эта. Именно в практических примерах и заключается основная ценность этой книги, которая очень пригодится любому, желающему стать экспертом по моделированию физики в играх. Я рекомендую вам прочитать эту книгу, независимо от того, отвечаете ли вы за моделирование физики, сценарий или интерфейс игры. Эта книга пригодится вам в любом случае. Искренне ваш, Андре Ламотте, редактор серии книг о разработке игр
Введение Добро пожаловать в мир физического моделирования*. Моделирование физических законов реального мира все шире используется в играх. Оно позволяет создавать» великолепно выглядящие игры, и оно является практически единственным инструментом, позволяющим создавать игры реалистичные. Компании, занимающиеся созданием компьютерных игр, постоянно ищут программистов, разбирающихся в физике. Хорошее знание физики может превратить человека с незначительными познаниями в программировании в ценного специалиста. Кроме того, моделирование физики - само по себе интересная задача. Простая физическая модель позволяет создавать эффекты, которых практически невозможно добиться без моделирования физики. Например, хорошая модель пламени будет прекрасно выглядеть, независимо от того, изображает ли она огонь в камине или горящий двигатель падающего самолета. Современные компьютерные игры в большинстве своем изображают целые виртуальные миры. Эти виртуальные миры могут выглядеть и функционировать так, как считают нужным их создатели. Однако если мы - создатели - хотим, чтобы игра была понятна игрокам и привлекала их, мы должны создавать миры, более или менее соответствующие реальности. А поведение и внешний вид реального мира - это и есть предмет физики. Однако не только понятность делает реальный мир хорошим примером для наследования. Реальный мир — удивительное место, и ни один выдуманный, виртуальный мир не моясет быть таким же замысловатым, богатым и прекрасным, как вселенная вокруг нас. Книга Книга разделена на три части. Часть первая. Физика, математика и программирование игр В первой части рассматривается математический аппарат, который потребуется нам для моделирования физики. В частности, рассматривается евклидова геометрия. Эта геометрия понадобится нам для работы с DirectX, которую мы тоже рассмотрим в первой части книги. Собственно говоря, графические возможности DirectX - просто удобный инструмент для изображения евклидовой геометрии в Windows.
Введение 9 Часть вторая: Трехмерные объекты, движение и столкновения Во второй части вы познакомитесь с динамикой материальных точек и твердых тел. Проще всего воспринимать динамику как науку о движении объектов. В этой части книги излагаются принципы и законы динамики, с помощью которых можно добиться реалистичного движения практически любых объектов в играх. Кроме того, мы разберемся, как изображать столкновения объектов - похоже, именно возможность врезаться во что-то пользуется особым спросом в играх. Физика, используемая в компьютерных играх, не ограничивается динамикой. В этой части вы также узнаете о силе тяжести, пружинах и жидкостях. Эти элементы часто используются в играх, чтобы добиться реалистичности, и чем мощнее становятся наши компьютеры, тем большей реалистичности можно добиться с их помощью. Часть третья: Практические трехмерные имитации Невозможно смоделировать что бы то ни было абсолютно точно. Вычислительная мощь компьютеров небесконечна, и это особенно заметно в играх, где любые вычисления нужно повторять не меньше 30 раз в секунду. Даже если вам хватает быстродействия компьютера, вы рано или поздно столкнетесь с чем-то, что невозможно точно смоделировать. И тогда приходится использовать приближения. Именно в этом и заключается суть имитаций. Третья часть посвящена самым распространенным типам имитаций в играх. Компакт-диск Компакт-диск, прилагаемый к книге, содержит множество полезных вещей. Прежде всего, на нем есть весь исходный код примеров к книге — вам не придется разбивать клавиатуру, набирая его. Весь исходный код находится в папке Source. В папке Tools вы найдете полезные для создания игр инструменты. Во-первых, в этой папке есть подпапка Microsoft DirectX SDK. В ней вы найдете копию набора инструментов, который Microsoft предлагает программистам для создания игр под DirectX. Если вы хотите использовать примеры кода из книги, вам понадобится установить этот набор. Кроме того, в папке Tools есть папка с замечательной маленькой программой MilkShape3D. Эта программа позволяет легко и быстро создавать трехмерные сетчатые модели. На компакт-диске находится пробная версия этой программы. Полнофункциональную версию стоимостью
10 Введение 20 долларов можно скачать с сайта разработчика - chUmbaLum sOf t — по адресу http://www.swissquake.ch/churabalum-soft/. Далее, в папке Tools есть папка Torque Game Engine — в этой папке находится демонстрационная версия полнофункционального игрового движка Torque. Этот движок создан компанией Garage Games. Веб-сайт этой компании можно найти по адресу www. garagegames . com. Если вы не можете себе позволить купить коммерческий игровой движок вроде Torque, попробуйте бесплатно распространяемый движок CrystalSpace 3D. Этот движок представляет собой проект с открытым исходным кодом. Он есть на компакт-диске в папке Tools\CrystalSpa- ce3D. Одно из самых удобных свойств этого движка - то, что вы можете изменять его исходный код так, как вам заблагорассудится. Проект CrystalSpace 3D в Интернете доступен по адресу: https://sourcefor- ge.net/projects/crystal. Что вам понадобится Чтобы воспользоваться большей частью изложенного в этой книге, вам понадобится компьютер с Windows 98 или более новой операционной системой и среда разработки программ на C++. Книга предполагает, что у вас есть доступ к Visual C++ 6.0 или более новой версии, но можно воспользоваться и другой средой. Кроме того, вам понадобится видеокарта, поддерживающая выходное разрешение 640 X 480 с 32-битным цветом. Я полагаю, что вы немного знаете С или C++. В книге используются базовые возможности C++. Многие концепции из физики и игр превосходно реализуются в виде объектов - так зачем же гробить себя, вбивая их в прокрустово ложе структур С? Если вы знаете только С или ваши навыки программирования на C++ немного заржавели, обратитесь к приложению В, «Краткий обзор языка C++». Вам не потребуются особые знания о программировании для Windows или DirectXrB первой части книги содержится достаточно информации, чтобы вы смогли приступить к написанию программ, использующих DirectX. Возможно, это введение в программирование покажется вам скуповатым, но изучите его, и вы сможете писать программы для Windows. Чтобы немного облегчить вашу задачу, в книге есть приложение С, «Основы программирования для Windows». Книга в основном посвящена физике и программированию игр, поэтому в ней немало математики. Многих людей математика отпугивает. Однако попробуйте почитать эту книгу, и вы увидите, что она не слишком сложна. На случай, если ваши школьные учителя математики до сих пор являются вам в кошмарных снах, я скажу вам, что очень часто математику представляют более сложной, чем она есть на самом деле. Все, что вам потребуется, чтобы понять эту книгу - быть готовым воспринять несколько новых идей. Вот, пожалуй, и все - можно приступать к первой главе, «Физика в играх».
Часть I Физика, математика и программирование игр Глава 1 Физика в играх 14 Глава 2 Имитация ЗБ-графики с помощью DirectX 21 Глава 3 Математические инструменты 53 Глава 4 2Б-преобразования и рендеринг 92 Глава 5 ЗВ-преобразования и рендеринг НО Глава 6 Сетчатые модели и Х-файлы 124
Глава 1 Физика в играх Что делает игры привлекательными? Люди, пишущие игры, тратят немало времени на поиски ответа на этот вопрос. Ответы, которые они дают, в основном зависят от аспектов игр, с которыми они работают. Писатели, маркетологи, производители и дизайнеры уровней ответят на этот вопрос по-разному. Однако подавляющее большинство игр связано с имитацией движения. Когда вы играете в игру, двигаются персонажи, объекты и сцены. Если вы хотите, чтобы игрок мог погрузиться в игровой мир, все должно двигаться реалистично. Это фундаментальный принцип игр. Чтобы добиться реалистичного движения персонажей и объектов на экране, нужно моделировать или имитировать физические законы реального мира. Современные игры обычно трехмерные. Так как же моделировать физический мир в 3D? Ответить на этот вопрос и призвана данная книга. Она позволит познакомиться с основами физики, математики и программирования трехмерных игр. Реакцией многих людей на предыдущее предложение будет паника. Люди часто приходят в ужас при мысля о физике и математике. Да, безусловно, физика и математика могут быть сложными. Однако есть важное правило, которое должны всегда помнить программисты, моделируя физику в играх: если все выглядит нормально, значит, все нормально. Это утверждение сильно облегчает программистам жизнь. Нам не нужно моделировать все аспекты физики, чтобы игры были реалистичными. Достаточно, если все выглядит правильным на экране. Такой подход освобождает нас, программистов, от необходимости работать с действительно замысловатыми областями физики и математики. Если мы знаем основы физики и обладаем некоторыми математическими навыками, этого почти наверняка хватит для программирования игр. Что я должен знать из физики, чтобы писать игры? Вспомните игры, в которые вы играли. Что происходит в этих играх? Ваш персонаж бегает и стреляет? Он лазит по лестницам, плавает, прыгает и так далее? Бросает ли он какие-то предметы? А взрывы? Похоже, что в наши дни игры просто не могут жить без взрывов.
Физика в играх 13 Встраивание физики в игры означает моделирование нескольких основных вещей: Q ЗБ-объектов; □ ЗВ-сцен; □ движения; □ твердых объектов; □ вращения; □ трения; Q сопротивления воздуха и воды; □ силы тяжести; □ столкновений и взрывов; □ гибких вещей; □ волн. ЗР-объекты Создать программную модель ЗВ-объекта непросто. Собственно говоря, чтобы понять, как это делать, потребовались десятилетия работы множества умных людей. Однако теперь, благодаря этим первопроходцам, мы знаем, как это делается. Мы можем использовать для имитации ЗБ-объектов широко доступные инструменты. Это очень сильно облегчает нам жизнь. Например, в большинстве современных компьютеров есть видеокарты, обладающие мощными возможностями по обработке ЗБ-графики. Это одновременно упрощает написание программ и ускоряет их работу. Графические библиотеки, например, BirectX и Open Graphics Library (OpenGL), дополняют аппаратную поддержку ЗБ-графики. Они добавляют еще один слой, выполняющий за нас часть работы. В главе 2 «Имитация ЗБ-графики с помощью BirectX» вы кратко познакомитесь с BirectX, поэтому, если вы не знаете, что это такое, не волнуйтесь. Вы увидите, что заставить BirectX работать несложно. ЗР-сцены Моделирование целых сцен в ЗБ - это продолжение темы моделирования ЗБ-объектов. Вы начнете моделировать ЗВ-сцены в главе 8 «Столкновения материальных точек». Движение В играх много движений. Части тел персонажей двигаются, когда персонажи ходят, прыгают, бегают или подбирают предметы. В сценах двигаются и персонажи, и предметы. Как сделать так, чтобы их движение выглядело реалистичным - эта тема затрагивается почти в каждой главе книги.
14 Глава 1 Твердые объекты Представьте себе, что вы пишете игру, в которой персонаж передвигается по внешней поверхности вращающейся космической станции. По мере перемещения персонажа от центра станции к ее краю силы, действующие на персонаж, растут. Чем ближе он к краю, тем больше у него вероятность сорваться и улететь в космос. Если он сорвется, игра окончена. Вращающаяся космическая станция - это пример движущегося твердого объекта. Твердые объекты кажутся обманчиво простыми. В действительности нужно сделать намного больше, чем кажется на первый взгляд. В главе 9 «Динамика твердых тел» и главе 10 «Столкновения твердых тел» вы познакомитесь с основами физики твердых тел. Вращение ЗБ-объекты могут двигаться вперед или назад, влево или вправо, вверх или вниз. Однако, двигаясь, они могут еще и вращаться. Моделирование вращения увеличивает количество сил, которые игра должна прикладывать к объекту. Вращение может стабилизировать или дестабилизировать движущиеся объекты. Например, когда игрок в футбол (я говорю об американском футболе) бросает мяч, он непроизвольно бросает его так, чтобы мяч вращался. Вращение стабилизирует полет мяча, и его легче поймать. Если вы пишете игру, в которой моделируется игра в футбол, моделирование вращения будет важным моментом. Трение В реальном мире большинство движущихся объектов, в конце концов, останавливается из-за трения. Моделирование трения часто бывает необходимым и в играх. Я играл в игры, где персонажу приходится двигаться по обледенелым или просто скользким поверхностям. Игрок должен добиться, чтобы персонаж двигался в нужном направлении по этим поверхностям, а чтобы сделать жизнь интереснее, по персонажу обычно стреляют со всех сторон. Мне часто приходилось сталкиваться со случаями, когда программисты теряли массу времени, пытаясь смоделировать трение. Они пытались свести все к правилу: «Если все выглядит нормально, значит, все нормально». Они писали программу и проверяли, правильным ли выглядело движение персонажа по скользкой поверхности. Если нет, они изменяли программу и снова проверяли. Поскольку они не опирались на реальную физику, им приходилось множество раз переписывать программу и проверять ее в работе. Они бы сэкономили массу времени, если бы просто использовали в программах формулы из физики.
Физика в играх 15 Сопротивление воздуха и воды Во многих играх сопротивление воздуха игнорируется вообще, однако никто этого не замечает. В прошлом игры выглядели правдоподобными и без моделирования сопротивления воздуха. Однако, похоже, это скоро отойдет в прошлое. Игры становятся все реалистичнее, и важность моделирования сопротивления воздуха растет. Игнорировать сопротивление воды разработчики игр не могут. В любой игре, где персонажи и объекты двигаются в воде, возникает необходимость реалистично моделировать сопротивление воды. Моделировать сопротивление воды значит не только замедлять движение в воде. Ведь вода может двигаться сама по себе. Возникающие при этом течения увеличивают сопротивление, если персонажи или объекты двигаются против этих течений. Течения увлекают за собой все в них попадающее. В некоторых играх движения в воде моделируются достаточно эффективно. Пример - серия игр Legend of Zelda. Главный персонаж часто движется в воде. При этом способ передвижения персонажа зависит от того, какими средствами на данный момент он располагает. Если у него есть волшебная маска, превращающая его в водное существо, он двигается быстро. В противном случае его движения будут довольно медленными. Течения увеличивают или уменьшают сопротивление, когда персонаж плывет. Если в вашей игре встречается движение в воде, то нужно смоделировать сопротивление воды хотя бы на том же уровне, что и в этой серии игр. Сила тяжести Сила тяжести влияет на все. От нее нельзя избавиться, даже в космосе. Не важно - бросает ли ваш персонаж гранату или ведет космический корабль к Марсу, на результат его действий влияет сила тяжести. Игра должна моделировать силу тяжести во всех ситуациях, поэтому моделированию силы тяжести я посвятил целую главу: это глава 11, «Сила тяжести и метательные снаряды». В ней рассказано достаточно, чтобы можно было смоделировать силу тяжести почти во всех ситуациях. Замечание Практически единственная ситуация, для которой я не описываю моделирование силы тяжести - это сила тяжести внутри черной дыры. Физические законы, которые нужны для моделирования этого случая, слишком сложны для этой книги. Если ваш персонаж в игре должен попадать в черные дыры, можете без угрызений совести обманывать игрока и программировать такое поведение, какое сочтете нужным. Игроку никогда не доводилось бывать в черной дыре, и он, скорее всего, поверит в то, что вы ему покажете
16 Глава 1 Столкновения и взрывы Что это за игра без взрывов? Даже в миролюбивых играх вроде The Sims есть взрывы. Я не знаю, почему это так, но большинству игроков нравится видеть, как предметы врезаются друг в друга и взрываются. Именно поэтому мы так часто видим сталкивающиеся автомобили в фильмах. Похоже, что Голливуд потребляет солидную часть продукции автопромышленности. Невозможно смоделировать все аспекты столкновений и взрывов. Физические соотношения, работающие здесь, слишком сложны. К счастью, это не так уж важно. Если мы сможем смоделировать основные силы и взаимодействия объектов в столкновениях и взрывах, все будет выглядеть нормально. А если все выглядит нормально, значит, все нормально. Гибкие вещи Хотя обычно мы этого не замечаем, вокруг нас множество гибких вещей. Если я говорю «гибкие вещи», вы, вероятно, представляете себе шесты для прыжков и тому подобное. В физике «гибкими вещами» будут, например, волосы и одежда. Представляли ли вы себе, как сложно смоделировать движение прически идущей девушки? Смоделировать движение платья ничуть не проще. Долгое время моделирование одежды, волос и других гибких вещей было слишком сложным, чтобы они могли присутствовать в играх. Более того, это моделирование было настолько сложным, что его избегали в компьютерной графике и анимации. ЗБ-моделирование тех времен было достаточно хорошим, чтобы моделировать все, кроме одежды и волос. В результате появилось множество ЗБ-мультфильмов о насекомых. В этих мультфильмах нет ни одежды, ни волос. Однако недавние достижения позволяют моделировать в играх гибкие вещи, включая одежду и волосы. Их движение можно сделать достаточно реалистичным. Волны Работая с водой, приходится иметь дело не только с сопротивлением и течениями: на воде должны быть волны. В старых играх волны имитировались медленным перемещением персонажа или камеры вверх и вниз. Но в современных ЗБ-играх такое не проходит. Нужен более реалистичный подход. Например, предположим, что вы пишете игру о гонках на моторных лодках, вроде Hydro Thunder (аркадная игра). Если волна ударит лодку в лоб, лодку может подбросить вверх или даже перевернуть. Если волна ударит лодку в борт, лодка вполне может перевернуться. Результат ударов зависит от размеров волн, угла столкновения лодки с ними, веса и формы лодки и так далее. Все эти факторы нужно правильно смоделировать в игре.
Физика в играх 17 Что я должен знать из математики, чтобы писать игры? Физика требует математики. Если вы не математический гений, не волнуйтесь, я расскажу вам обо всех математических понятиях, которые вам потребуются, чтобы разобраться в этой книге. Вы познакомитесь с: □ основами геометрии треугольников; □ векторами; □ матрицами; □ производными. Основы геометрии треугольников Компьютерная ЗБ-графика основана на треугольниках. Если вы собираетесь моделировать ЗБ-сцены и объекты, вы должны знать основные свойства треугольников. Например, нужно уметь найти длину стороны треугольника, зная длину двух других сторон и величину угла между ними. Векторы Физика занимается вопросами взаимодействия сил и объектов. Силы очень удобно представлять с помощью векторов. Векторы дают удобный способ анализа сочетаний сил и определения сил, действующих на объекты. Матрицы Программисты, работающие с ЗБ-объектами, обычно преобразуют векторы сил в матрицы. Матрицы предоставляют изящный способ упрощенного представления проблем. Они делают многие задачи ЗБ-графики более простыми для понимания и выполнения. Например, предположим, что нужно смоделировать поведение ящика, которое должно зависеть от того, куда приложено усилие - к ребру или к середине грани. Если усилие приложено к верхнему ребру, он должен перевернуться. Если оно приложено к середине боковой грани, он должен скользить по полу. Чтобы правильно смоделировать поведение ящика, нужно начать с анализа сил с помощью векторов. Затем нужно преобразовать векторы в матрицы и воспользоваться правилами умножения матриц, чтобы определить величины сил, действующие на вершины ящика. В результате можно определить, как будет двигаться ящик.
18 Глава 1 Описанная только что методика применяется при решении многих задач. Умение обращаться с матрицами крайне важно для программиста, пишущего игры. Производные Производные - это часть математического анализа. Да, математический анализ - не самая простая область математики, и он сложен для понимания. Однако сам процесс использования производных можно упростить. Если вы не изучали математический анализ, я думаю, вы удивитесь тому, насколько простыми могут быть производные. Что я должен знать из программирования? Краткий ответ на этот вопрос: «Не слишком много». Если вы можете написать программу на C++ для Windows, то знаете достаточно, чтобы освоить эту книгу. Если вы изучали программирование на C++ в школе или институте, то поймете все, что мы будем рассматривать в этой книге. Если вы изучали программирование самостоятельно и занимались написанием программ на C++ для Windows около года или больше, все будет в порядке. Для программирования графики мы будем использовать библиотеки Microsoft DirectX. О DirectX написаны целые книги. Вам не обязательно иметь опыт работы с ним. В этой книге о DirectX будет рассказано достаточно, чтобы вы смогли выполнять физическое ЗО-моделирование, которому посвящена данная книга. Если вы хотите глубже изучить DirectX, попробуйте почитать, например, книгу Wendy Jones «Beginning DirectX 9» (издательство Premier Press). Если вы приверженец OpenGL или какой-то другой графической библиотеки, не пугайтесь. Хотя в примерах этой книги используется DirectX, собственно физическое моделирование выполняется в коде, который можно использовать практически с любой графической библиотекой. Можно использовать этот код с OpenGL или чем-то еще, не внося в него больших изменений. Итоги Чтобы создать реалистичную 3D-nrpy, программисты должны моделировать физические силы, действующие в природе. Это требует знания физики, математики и программирования ЗО-графики. Обо всем этом и рассказывается в этой книге. Прежде всего, мы изучим основы 3D-npo- граммирования с помощью DirectX. Этому посвящена следующая глава.
Глава 2 Имитация Зй-графики с помощью DirectX В этой главе мы познакомимся с API DirectX, созданным Microsoft. DirectX — основной инструмент, используемый создателями игр и графических программ для работы с ЗО-графикой. Если вы уже знакомы с DirectX, возможно, вы захотите сразу перейт^и к главе 3, «Математические инструменты», и начать знакомиться с математикой, которая нам понадобится. Если вы впервые столкнулись с DirectX, то эту главу придется прочитать. В ней есть: □ обзор DirectX и его возможностей; □ введение в компоненты DirectX; Q пошаговое руководство по подготовке DirectX к работе; □ обзор операций, которые должна выполнить программа, завершая работу с DirectX. Что такое DirectX? DirectX - это интерфейс программирования приложений (API - Application Programming Interface), позволяющий разработчикам игр и графических приложений выполнять мультимедиа-задачи, не привязываясь к конкретным типам аппаратных устройств. Это избавляет вас и меня от забот о том, какие видеокарты и звуковые карты установлены в компьютерах пользователей. Кроме того, DirectX предоставляет высокоуровневые функции для выполнения множества задач, связанных с ЗО-графикой. Это позволяет нам с легкостью сконцентрироваться на собственно играх, а не на задачах генерации графики и звука. Необходим ли нам DirectX? Если коротко, то да. Чтобы понять, почему, подумайте, как работает большая часть программ. Подавляющее большинство приложений большую часть своего времени взаимодействуют с Windows, используя обработчики событий. Например, Windows сообщает приложению, что была нажата кнопка Close в его окне или пользователь щелкнул левой кнопкой мыши в какой-то точке окна. Приложение реагирует на эти сообщения. Например, оно может попросить Windows создать окно или нарисовать линию. Windows выполняет полученные запросы.
20 Глава 2 У такого подхода есть свои преимущества. Он использует API, который называется GDI (Graphics Device Interface - интерфейс графических устройств), чтобы позволить программистам писать программы, не беспокоясь о том, какие графические карты установлены в компьютерах пользователей. Он также вынуждает приложения аккуратно обращаться с другими приложениями. И, наконец, он позволяет пользователю легко копировать данные из одних приложений в другие. Но для игр GDI работает слишком медленно. GDI создан для рабочих приложений (например, для рисования диаграмм), которые не слишком быстро изменяются во времени. Его невозможно использовать для отображения ЗО-графики в реальном времени. Альтернативы DirectX DirectX - не единственный существующий игровой API. У разных частей DirectX есть серьезные соперники. Например, библиотека OpenGL (Open Graphics Library - открытая графическая библиотека) - хорошая альтернатива Direct3D, OpenAL (Open Audio Library - открытая аудио библиотека) - альтернатива DirectSound, a Berkeley Sockets может выполнять большинство функций DirectPlay. Преимущество этих API перед DirectX - возможность применения их на разных платформах. OpenGL можно использовать на машине под управлением Windows, на машине Apple или машине под управлением Linux, a DirectX работает только на компьютерах под управлением Windows. Преимущество DirectX - принадлежность к вселенской империи; DirectX работает хорошо на большинстве машин, поскольку на большинстве машин используется Windows, и в поддержку DirectX вкладывается много денег и усилий. Хотя эта книга концентрируется на использовании DirectX для ЗО-моделиро- вания, физика и описывающий ее код остаются неизменными при использовании любого API. Можете использовать то, что вам нравится. Два представления DirectX Microsoft делит интерфейс DirectX на два основных набора API. Один набор - низкоуровневый и напрямую обращается к аппаратным устройствам. Если нужных аппаратных устройств в системе нет, этот низкоуровневый API имитирует их присутствие. DirectX также содержит набор высокоуровневых API, к которым можно обращаться через программные объекты, содержащиеся в библиотеках DirectX. Низкоуровневое представление: HAL и HEL DirectX позволяет программистам работать с аппаратными устройствами практически напрямую, при этом сохраняя аппаратную независимость, обеспечиваемую мультимедиа-стандартами Windows. He важно, какие видеокарты и звуковые карты установлены в компьютерах игроков - DirectX позволяет их использовать. Команды DirectX преобразуются
Имитация ЗР-графики с помощью DirectX 21 непосредственно в команды, понятные аппаратным устройствам в компьютере пользователя. Рисунок 2.1 показывает, как это делается. Как видите, есть два компонента, отделяющие высокоуровневый API DirectX от аппаратных устройств: HAL (Hardware Abstraction Layer - слой абстрагирования аппаратуры) и HEL (Hardware Emulation Layer - слой эмуляции аппаратуры). Приложение Win32 хг Компоненты DirectX 1' HEL. слой эмуляции аппаратуры > ' 1' HAL: слой абстрагирования аппаратуры 1 Аппаратные устройства, видеокарты, звуковые карты, джойстики и так далее Рис. 2.1. Архитектура DirectX HAL преобразует инструкции DirectX в инструкции аппаратуры. Чтобы игры работали как можно быстрее, DirectX пытается выполнять все задачи с помощью аппаратных устройств. Для этого он использует HAL всегда, когда это возможно. А что, если окажется, что аппаратура не поддерживает какую-то возможность, запрошенную DirectX? DirectX притворится, что эта возможность поддерживается аппаратурой. Да, именно так. Он воспользуется вторым низкоуровневым API - HEL. HEL эмулирует возможности, отсутствующие в аппаратных устройствах компьютера. Это позволяет играм работать, если DirectX требует больше, чем могут предоставить устройства компьютера. Но у эмуляции есть своя цена. HEL работает очень медленно. Высокоуровневое представление: компоненты DirectX В высокоуровневом представлении DirectX делится на несколько компонентов, большинство из которых мы будем использовать, изучая моделирование физики: Q Direct3D, Этот компонент отвечает за работу с графикой - как 2D, так и 3D. Когда-то за 20-графику отвечал DirectDraw, но Microsoft встроила его в Direct3D и переименовала в DirectX Graphics. Однако почти все называют его Direct3D. Direct3D позволяет работать с 2Р-графикой, используя все возможности аппаратных устройств, которыми обладают ЗО-графические карты. В этой книге мы будем использовать и 2D-, и ЗО-графику.
22 Глава 2 □ Directlnput. Этот компонент обеспечивает поддержку мышей, клавиатур, джойстиков, трекболов и практически любых других устройств ввода. Microsoft говорит производителям устройств ввода: «Если вы хотите, чтобы они работали в Windows, лучше напишите для них драйверы под Directlnput». Интерфейс Directlnput настолько абстрактен, что фактически производители могут создать под него драйверы для чего угодно - от трекболов до костюмов виртуальной реальности. □ DirectPlay. Этот компонент обеспечивает сетевые многопользовательские игры. Когда вы используете DirectPlay, не важно, подключаетесь ли вы к сети через модем, Internet-канал, LAN или что-то еще; обо всех связанных с аппаратурой вопросах позаботятся за вас. В этой книге DirectPlay не рассматривается, но, став гением физики, можете использовать его для моделирования физики в многопользовательских играх. □ DirectSound. Этот компонент отвечает за работу с цифровым звуком. Он позволяет обращаться непосредственно к звуковой карте, не зная, какого она типа, и автоматически использует предоставляемые этой картой возможности ускорения и специальные возможности. Также он поддерживает ЗО-звучание и звуковые эффекты. □ DirectMusic. Как явствует из названия, этот компонент воспроизводит музыку, но он делает и намного больше. Источники музыки могут по-разному размещаться в ЗО-среде, и их звучание может динамически изменяться. DirectMusic может даже создавать композиции во время работы на основе элементов, которые вы ему передадите. Q DirectShow. Этот компонент отвечает за запись и воспроизведение мультимедиа-потоков, например, MPEG, AVI и МРЗ. СОМ-объекты DirectX теоретически основывается на объектах, созданных с помощью модели Microsoft COM (Component Object Model - компонентная модель объектов). СОМ - это абстракция, изобретенная для упрощения больших программных проектов. "Удачной ли была эта абстракция, каждый может решать сам. Идея состоит в том, что каждый СОМ-объект представляет собой черный ящик, соответствующий какой-то части программы или аппаратному устройству. Чтобы создать программу, вы связываете между собой набор объектов. Доступ к СОМ-объектам осуществляется через интерфейсы. Интерфейс (interface) — это набор функций, называемых методами (methods). Большая часть сказанного будет звучать знакомо для программистов, работавших на объектно-ориентированных языках, например, C++ или Java. Собственно говоря, объекты СОМ совместимы с объектами C++ на
Имитация ЗР-графики с помощью DirectX 23 бинарном уровне; в программах на C++ объекты СОМ могут использоваться как обычные объекты. СОМ-объекты компонуются динамически во время выполнения программ. Это значит, что, в идеале, СОМ-объекты можно заменять в программах на новые объекты без необходимости перекомпилировать программу. Это полезно, если мы хотим обновить распространенную и широко используемую программу или большую систему. СОМ-объекты обладают достаточными возможностями, чтобы эта операция была эффективной. □ У каждого СОМ-объекта и интерфейса есть уникальный 128-битовый идентификационный номер, который называется глобально уникальным идентификатором (GUID - Globally Unique IDenti- fier). Созданная Microsoft программа GUIDEN.EXE генерирует эти идентификаторы, которые будут уникальными; скорее всего, никакие два СОМ-объекта или интерфейса, созданные кем угодно, где угодно и когда угодно, не будут иметь одинаковых идентификаторов. □ Обновленные версии СОМ-объектов должны поддерживать интерфейсы предыдущих версий. При этом программы, в которых используется этот СОМ-объект, будут продолжать работать без перекомпиляции, даже если внутреннее содержимое объекта полностью изменилось. Q СОМ-объекты содержат счетчик, отслеживающий количество активных ссылок на эти объекты. Если это количество равно О, ресурсы, выделенные объекту, освобождаются, и объект уничтожается. СОМ является основой ActiveX, OLE и, что важнее всего для нас, DirectX. COM, ActiveX и .NET Сначала DirectX не был API, основанным на COM. Microsoft добавила СОМ к DirectX, купив этот API у создавшей его компании Reality Labs. В результате присутствие СОМ в DirectX не слишком навязчиво. Да, СОМ нужно использовать для выделения и освобождения основных компонентов DirectX. Но кроме этого, больше возиться с СОМ не нужно. Не обязательно быть знатоком СОМ и разбираться в его тонкостях, чтобы использовать в своих программах DirectX. Вы наверняка слышали о .NET - инициативе компании Microsoft. Все, что делает Microsoft, перетаскивается под вывеску .NET. Это относится и к DirectX. Microsoft уже предоставила доступ к DirectX для программ .NET. Интерфейс .NET использовать проще, чем интерфейс СОМ. Однако использование интерфейса .NET связано с некоторыми накладными расходами. Снижение скорости работы программ при использовании .NET составит 2-5 %. Кроме того, может увеличиться размер программ. Вам решать, стоят ли увеличение размера и падение скорости работы программ облегчения их разработки.
24 Глава 2 Использование DirectX Есть три способа, позволяющие запустить DirectX и воспользоваться его функциональностью. Первый - лобовой способ. Нужно создать набор переменных для инициализации и передать информацию из них функциям инициализации. Когда DirectX будет готов к работе, к его функциональности можно обращаться через его API. Второй способ — позволить Visual Studio сделать часть работы за вас. Когда вы устанавливаете DirectX SDK (Software Development Kit - набор разработки программ), он автоматически добавляет мастер DirectX Арр- Wizard к Visual Studio. AppWizard создает для вас пустые DirectX-приложения. Все, что остается сделать вам - добавить в них функциональность игр или графических программ. Третий способ - самый простой. Позвольте мне сделать за вас часть работы. По разным причинам DirectX App Wizard обладает некоторыми ограничениями. Есть и некоторые недостатки в его использовании. Поэтому я создал оболочку исходного кода, которая запустит и подготовит DirectX к использованию. Использовать App Wizard и эту оболочку удобно, но есть вещи, которые нельзя сделать с их помощью. Поэтому важно разбираться в API DirectX. И мы кратко рассмотрим инициализацию части DirectX - a точнее, Direct3D - лобовым способом. Это позволит нам узнать, как DirectX работает в действительности. После этого я познакомлю вас с мастером App Wizard и созданной мной средой. Инициализация DirectX лобовым способом Компоненты DirectX - это СОМ-объекты. СОМ-объекты реализуются в виде библиотек DLL (Dynamic Link Library - библиотека динамической компоновки). Когда вы играете в игру, использующую DirectX, эти библиотеки загружаются, и игра запрашивает из них нужные ей интерфейсы. Методы из этих интерфейсов и выполняют все операции рисования, работы со звуком и обработки ввода. Написание собственных СОМ-объектов возможно и, вероятно, полезно, но большинству программистов, пишущих игры, достаточно уметь использовать эти объекты, связанные с DirectX. На самом деле мы практически не будем иметь дела с СОМ-объектами DirectX. Microsoft знала, что СОМ-объекты в DirectX должны присутствовать в минимальном количестве, чтобы DirectX получил распространение, и спрятала большую часть взаимодействия с СОМ в пару функций, содержащихся в библиотеках импорта. Это удобно, поскольку всю функциональность, которая вам может понадобиться в DirectX, можно получить, не работая прямо с СОМ.
Имитация ЗР-графики с помощью DirectX 25 Несколько слов о стиле оформления программ Код в этом разделе оформлен в стиле, используемом Microsoft. В следующем разделе мы будем рассматривать код, сгенерированный мастером AppWi- zard. Этот мастер тоже генерирует код, оформленный в стиле Microsoft. Мой собственный стиль оформления кода довольно сильно отличается от стиля Microsoft. Тому есть свои причины, однако я не собираюсь стоять насмерть, если мой стиль вас не устраивает. В оставшейся части книги я буду использовать стиль Microsoft для кода, выполняющего инициализацию DirectX и завершение его использования. Все остальное будет оформлено в моем стиле. Это поможет вам различать код, важный для приложения, от общего для всех игр кода инициализации и завершения. Использование СОМ-объекта DirectX состоит из четырех шагов. 1. Объявление переменной, в которой будет храниться указатель на интерфейс объекта. Сначала этой переменной присваивается значение NULL: LPDIRECT3D g_pD3D = NULL; 2. Вызов функции создания объекта. Эта функция возвращает указатель на интерфейс объекта, который можно хранить в созданной в шаге 1 переменной. Если функции не удается создать объект, она возвращает значение NULL: g_j>D3D = Direct3DCreate9( D3D_SDK_VERSION ); 3. Теперь, когда у нас есть указатель на интерфейс, можно использовать его для вызова методов. Например: g_pD3D-X3etAdapterDisplayMbde( D3DADAPTER_DEFAULT, Scurrentdisplay); 4. Завершив работу с СОМ-интерфейсами, нужно освобождать их в порядке, обратном порядку их инициализации. Невыполнение этой операции приведет к утечкам ресурсов, замедлению работы систем и появлению кровожадных игроков, целью существования которых является ваша преждевременная кончина. g_pD3D->Release(); g_pD3D = NULL; Замечание Код, показанный выше, используется для инициализации Direct3D. Код для инициализации других компонентов DirectX имеет ту же структуру. Теперь вы знаете о СОМ достаточно, чтобы инициализировать Di- rect3D. Попробуем это сделать.
26 Глава 2 ИНИЦИАЛИЗАЦИЯ DIRECT3D Direct3D работает и с 2D-, и с ЗБ-графикой. Это делает Direct3D самым важным для нас компонентом DirectX - обо всем, что происходит в играх, мы узнаем через экран монитора. По мере усложнения физических моделей мы будем все более интенсивно использовать Direct3D. А сейчас мы просто проинициализируем его и настроим экран. Каждый раз, когда вы создаете проект, использующий DirectX, нужно добавить к нему библиотечные файлы DirectX, которые вам понадобятся. Если вы работаете в Visual Studio 6, откройте меню Project и щелкните в нем на пункте Settings. Откроется диалоговое окно Project Settings. Щелкните на вкладке Link. Найдите текстовое поле Object Library Modules. В этом поле введите имена библиотечных файлов, которые вам понадобятся. Для большинства приложений, использующих DirectSD, будет достаточно ввести следующее: dxguid.lib d3d9.1ib d3dx9.1ib winmm.lib Замечание Если вы используете Visual Studio .NET, щелкните правой кнопкой на имени проекта. Из открывшегося контекстного меню выберите пункт Properties. В появившемся окне щелкните на папке Linker и выберите в этой папке пункт Input. Введите приведенный выше список библиотек в строке Additional Dependencies. В программу нужно включить заголовочный файл DirectX 9: #include <d3d9.h> // DirectX Version 9 Теперь создадим переменную, в которой будет храниться указатель на интерфейс. Большинство разработчиков игр делают такие переменные глобальными: LPDIRECT3D9 g_pD3D = NULL; // Указатель на объект Direct3D Подсказка Как бы я ни ненавидел глобальные переменные, избежать их использования при программировании игр сложно. Передача параметров функциям, от которых требуется максимальная скорость работы, может замедлить игру до невозможности, если эти параметры нужно помещать в стек и извлекать из него. Увы - глобальные переменные работают быстрее. Это не значит, что у вас нет выбора и придется использовать глобальные переменные во всех случаях. Будьте осторожны, выбирая, какие переменные сделать глобальными и как к ним обращаться. Глобальные переменные могут стать источником ошибок, которые будет трудно устранить.
Имитация ЗР-графики с помощью DirectX 27 Я помещу собственно инициализацию DirectX в новую функцию, названную Direct3DInit (). Эта функция будет вызываться из функции Gamelnit(). Объекты Direct3D можно создавать с помощью функции Direct3DC- reate9 (). Я назову возвращаемое значение этой функции его официальным именем - IDirect3D9. Здесь / означает интерфейс (interface). // Получаем указатель на IDirect3D9 if (NULL == ( g_j>D3D = Direct3DCreate9( D3D_SDK_VERSION ) ) ) return E_FAIL; Функции Direct3DCreate9 () всегда нужно передавать значение D3D_SDK_VERSION. Других корректных значений нет. D3D_SDK_ VERSION обновляется при обновлении DirectX. Передача этого параметра сообщает программе, с какой версией DirectX ей нужно работать. Функция возвращает NULL, если ей не удается создать объект Di- rect3D. Если она возвращает NULL, функция Gamelnit() возвращает значение E_FAIL. Используя в программах СОМ, вы будете довольно часто встречать значения S_OK и E_FAIL. Все методы СОМ-объектов возвращают 32-разрядные целые значения типа HRESULT, сообщающие о результатах работы этих методов. Обычно возвращаются коды S_OK и E_FAIL, но иногда метод может вернуть нечто вроде E_INVALDARG, если ему передали неправильные аргументы, так что будьте внимательны. Согласно принятым стандартам, при успешном выполнении методы возвращают коды, начинающиеся с S, а при неудаче - коды, начинающиеся с Е. Если вы захотите узнать, успешно ли выполнился метод, воспользуйтесь следующими макросами: □ SUCCEEDED. Возвращает TRUE для кодов успешного выполнения и FALSE - для кодов неуспешного. □ FAILED. Возвращает TRUE для кодов неуспешного выполнения и FALSE — для кодов успешного. Поскольку мы возвращаем функции Gamelnit() либо значение S_OK, либо значение E_FAIL, то успешность выполнения этой функции можно выяснить в функции WinMain () с помощью макроса FAILED: // Инициализация игровой консоли if ( FAILED ( GamelnitO ) ) return @); Если Gamelnit() возвратит код ошибки, WinMain () выразит возмущение.
28 Глава 2 РЕЖИМЫ ДИСПЛЕЯ Теперь, получив интерфейс объекта, можно воспользоваться его методами. Для начала узнаем, какой сейчас используется режим дисплея: // Структура для хранения информации о текущем режиме дисплея D3DDISPLAYM0DE currentDisplay; // Получаем информацию о текущем режиме дисплея if ( FAILED( g_pD3D-> GetAdapterDisplayMode( D3DADAPTER_DEFAULT, ScurrentDisplay ) ) ) { return E_FAIL; } Подсказка К методу интерфейса можно обратиться, указав имя интерфейса, за которым следуют два двоеточия (::), а затем имя нужного метода. Поэтому, если я пишу IDirect3D9: :GetAdapterDisplayMode (), я обращаюсь к методу GetAdapterDisplayMode () интерфейса IDirect3D9. Метод IDirect3D9::GetAdapterDisplayMode() принимает два параметра. Первый — используемый адаптер. Значение D3DADAPTER_ DEFAULT соответствует основному адаптеру. Второй параметр - указатель на структуру, в которой хранится информация о режиме дисплея. Посмотрим на определение этой структуры: typedef struct _D3DDISPLAYMODE { UINT Width; UINT Height; UINT RefreshRate; D3DFORMAT Format; } D3DDISPLAYMODE; Смысл первых трех параметров вполне очевиден. Это ширина и высота изображения на дисплее в пикселях, например, 1280 х 1024, и частота обновления кадров, например, 85 Гц. Последний параметр - формат поверхности. Он показывает, как воспринимается информация о каждом пикселе. Есть много разных форматов, из которых только несколько подходят для дисплеев и видеостраниц (мы вскоре разберемся, что такое видеостраницы). В таблице 2.1 перечислены эти типы.
Имитация ЗР-графики с помощью DirectX 29 Таблица 2.1. Типы D3DFORMAT Формат Значение D3DFMT_A2R10G10B10 32-битовый формат пикселя, в котором по 10 битов используется для каждого цвета, а 2 бита - для альфа-канала D3DFMT_A8R8G8B8 32-битовый формат пикселя ARGB, в котором по 8 битов используется для каждого цвета и еще 8 - для альфа-канала D3DFMT_X8R8G8B8 32-битовый формат пикселя, в котором по 8 битов используется для каждого цвета D3DFMT_A1R5G5B5 16-битовый формат пикселя, в котором по 5 битов используется для каждого цвета, а 1 бит - для альфа-канала D3DFMT_xiR5G5B5 16-битовый формат пикселя, в котором по 5 битов используется для каждого цвета D3DFMT_R5G6B5 16-битовый формат пикселя, в котором 5 битов используется для красного цвета, 6 - для зеленого и 5 - для синего Если функция IDirect3D9: : GetAdapterDisplayMode () отработала как должно, то информация о текущем режиме дисплея хранится в структуре currentDisplay. ПАРАМЕТРЫ ДИСПЛЕЕВ Все, что отображается на дисплее, копируется непосредственно из области памяти, называемой текущей видеостраницей (front buffer). Эта область может располагаться в основной оперативной памяти компьютера, но чаще она находится в памяти видеокарты. Когда программа (или Windows) желает отобразить на экране что-то новое, она изменяет содержимое этой области (буфера), и графическая карта пересылает это содержимое монитору. Размер буфера зависит от разрешения монитора и используемой глубины цвета. Разрешения, которые можно использовать, ограничиваются монитором, видеокартой и Windows. Если вы хотите использовать какое-то конкретное разрешение, его должны поддерживать и монитор, и видеокарта, и Windows. Если вы играли в компьютерные игры или просто возились с настройками дисплея в Control Panel, вы, вероятно, знакомы с самыми распространенными разрешениями, например, 640 х 480, 800 X 600, 1024 х 768, 1280 х 1024 и 1600 х 1200. Кроме разрешения дисплея, важна еще глубина цвета. Глубина цвета — это количество памяти, отвечающее одному пикселю на дисплее. Большая часть этой памяти предназначена для хранения цвета пикселя.
30 Глава 2 Например, если используется 16-битовая глубина, каждый пиксель может принимать 216 или 65536 цветов. Если глубина - 24 бита, то разных цветов может быть 224 или более 16.7 миллиона. Это больше, чем могут различить ваши глаза! Экран монитора покрыт миллионами светоизлучающих элементов, объединенных в тройки. Каждая тройка содержит один красный светоиз- лучающий элемент, один зеленый и один синий. Сочетая разные интенсивности свечения этих элементов, можно получить любой цвет. Такая система называется RGB (Red-Green-Blue - красный-зеленый-синий). Замечание В большинстве телевизоров и мониторов с ЭЛТ эти тройки состоят из люминофоров, светящихся, если их бомбардировать электронами. Такое решение стало стандартом в 1930-х годах, когда создавалось цветное телевидение, и используется до сих пор, хотя в последнее время появляются новые технологии. Эти технологии работают по-разному, но, в общем, все они тоже используют RGB-цвета. Часто биты, используемые для хранения цвета, делятся между этими тремя излучателями. Например, в 16-битовом формате R5G6B5 5 битов предназначены для хранения данных об интенсивности красного цвета, 6 битов - зеленого и последние 5 битов — синего. Если люди не могут различить более 16.7 миллионов цветов, зачем использовать еще большую глубину цвета? Дело в том, что дополнительные биты можно использовать для хранения другой информации. Чаще всего в ней хранятся данные для альфа-канала, позволяющего реализо- вывать эффекты прозрачности. ПОВЕРХНОСТИ И ПЕРЕКЛЮЧЕНИЕ ВИДЕОСТРАНИЦ В Direct3D области памяти, соответствующие по размеру экрану, называются поверхностями (surfaces). Выполняя рисование с помощью Direct3D, вы на самом деле рисуете на поверхности неактивной видеостраницы (back buffer), а монитор в это время отображает содержимое активной видеостраницы (front buffer). Неактивная страница не отображается; это просто область памяти, имеющая тот же размер и ту же организацию, что и активная видеостраница. Когда вашей программе нужно изменить изображение, она записывает новые данные в неактивную видеостраницу. Когда процесс рисования заканчивается, мы просто меняем указатели местами, и поверхность, бывшая неактивной видеостраницей, становится активной, а бывшая активной - становится неактивной. Этот процесс называется переключением видеостраниц (page flipping) - (см. рис. 2.2). Использование переключения страниц решает несколько проблем. Во-первых, если рисовать непосредственно на активной видеостранице, изображение может быть разорвано. Разрыв (tearing) появляется, если содержимое активной видеостраницы изменяется в тот момент, когда монитор обновляет изображение на экране. При этом монитор отобразит
Имитация ЗР-графики с помощью DirectX 31 часть обновленного содержимого видеостраницы и часть необновленного. Если использовать переключение страниц, разрывы видны не будут, поскольку изменения не появятся в активной странице, пока программа не закончит рисовать. Изначально на мониторе отображается первая поверхность, а на второй - идет рисование Первая поверхность Активная видеостраница Вторая поверхность Неактивная видеостраница Когда рисование заканчивается, вторая поверхность становится активной видеостраницей и отображается на мониторе. После этого можно начать перерисовывать первую поверхность Рис. 2.2. Схема переключения видеостраниц Неактивная видеостраница Активная видеостраница Активная видеостраница Неактивная видеостраница Кроме того, переключение видеостраниц позволяет перезаписывать содержимое видеостраницы, не отображая ее. Почему это может понадобиться? Например, это нужно в псевдо-трехмерных изометрических играх, скажем, в Diablo. Изометрические игры выглядят трехмерными, но в них позиция наблюдения фиксирована, и линия взгляда обычно находится под углом около 45° к горизонту. Когда прорисовывается персонаж игрока и существа, стремящиеся его уничтожить, нужно проследить, чтобы более близкие объекты прорисовывались поверх более дальних.
32 Глава 2 Это не слишком сложно. Нужно просто прорисовывать первыми самые дальние объекты, а затем прорисовывать более близкие. Это хорошо получается, если рисовать на неактивной видеостранице и переключать видеостраницы, закончив рисовать, но если рисовать на активной видеостранице, то иногда объекты заднего плана могут оказываться нарисованными поверх объектов переднего плана. СОЗДАНИЕ УСТРОЙСТВА Теперь мы готовы использовать интерфейс IDirect3D9, чтобы создать еще один объект - устройство (device). Объект Direct3D был довольно абстрактен, но устройство - это гораздо более конкретная вещь, соответствующая аппаратному устройству: видеокарте. Все рисование, которое вы хотите выполнять с помощью этой графической карты, будет выполняться через создаваемый сейчас интерфейс. Если вы захотите использовать вторую видеокарту (это делают немногие игры), вам понадобится второе устройство. Чтобы создать устройство, нужно следовать той же процедуре, что и при создании других объектов. Сначала нужно объявить указатель на интерфейс IDirect3DDevice9. Часто этот указатель делают глобальным, чтобы к нему можно было обращаться из любой точки программы, в частности, из функции GameLoop (), в которой будет выполняться рендеринг: LPDIRECT3DDEVICE9 g_pDevice = NULL; // Наше устройство рендеринга Устройство создается с помощью метода IDirect3D9: :CreateDe- vice(). Этот метод использует довольно много параметров, как видно из прототипа: HRESULT CreateDevice( UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow, DWORD BehaviorFlags, D3DPRESENT_PARAMETERS *pPresentationParameters, IDirect3DDevice9** ppReturnedDevicelnterface ); В первом параметре этой функции — Adapter - указывается адаптер, которому будет соответствовать создаваемое устройство. В большинстве случаев этому параметру предается значение D3DADAPTER_DEFAULT. В параметре DeviceType указывается, будет ли использоваться для растеризации и расчета освещения аппаратное устройство или программа. Три возможных варианта перечислены в таблице 2.2. Растеризация (rasterization) - это процесс преобразования геометрических фигур, например, линий и поверхностей, в пиксели, которые можно отобразить на экране.
Имитация ЗР-графики с помощью DirectX 33 Таблица 2.2. Возможные значения параметра DeviceType Значение Смысл D3DDEVTYPE HAL D3DDEVTYPE HEL D3DDEVTYPE SW Использовать аппаратную растеризацию. Все эффекты теней и освещения рассчитываются аппаратно, если это возможно. Это значение используется чаще всего Все рассчитывается программно. Это значение просто является приказом использовать HEL. Это медленно, но иногда полезно при отладке Это подключаемое программное устройство. Значение используется, если вы хотите написать собственное устройство рендеринга. Это не так просто Параметр hFocusWindow указывает, в каком окне будет выполняться рисование. Следующий параметр - BehaviorFlags - содержит несколько флагов общего характера, описывающих требуемое от устройства поведение. Один из этих флагов указывает, должны ли вершины или вертексы (vertices) обрабатываться программно или аппаратно. В таблице 2.3 перечислены наиболее распространенные значения этого параметра. Таблица 2.3. Флаги поведения для устройств Флаги Значение D3DCREATE_HARDWARE_ VERTEXPROCESSING D3DCREATE_SOFTWARE_ VERTEXPROCESSING D3DCREATE_MIXED_ VERTEXPROCESSING D3DCREATE_DISABLE_ DRIVER_MANAGEMENT D3DCREATE MANAGED D3DCREATE_ MULTITHREADED Вершины обрабатываются аппаратно Вершины обрабатываются программно Смешанная обработка вершин; иногда DirectX будет использовать программы, а иногда - аппаратные устройства Вместо драйвера распоряжаться ресурсами будет Direct3D Ресурсы перемещаются между оперативной памятью и ускорителем по мере надобности. Это освобождает приложение от возни с распределением памяти Это заставляет устройство обеспечивать безопасность при многопоточном использовании. При этом снижается производительность
34 Глава 2 Параметр pPresentationParameters функции CreateDevice () - это указатель на структуру (весьма сложную), которая указывает способ отображения результатов работы. Например, эта структура определяет формат видеостраниц и то, рисует ли программа в окне или в полноэкранном режиме. Последний параметр функции CreateDevice () — ppReturnedDevi- celnterface. Он определяет адрес указателя на интерфейс, который вы создаете с помощью этой функции. Прежде чем вы сможете создать устройство, нужно заполнить структуру pPresentationParameters. Вот ее объявление: // Структура для хранения информации о методе рендеринга D3DPRESENT_PARAMETERS d3dpp; С элементами структуры вы можете познакомиться в таблице 2.4, но большую их часть мы установим в О с помощью макроса ZeroMemory (>: // Инициализация d3dpp в 0. ZeroMemory( &d3dpp, sizeof( D3DPRESENT_PARAMETERS ) ); Таблица 2.4. Элементы структуры D3DPRESENT_PARAMETERS Элемент Описание BackBuf£erWidth, BackBufferHeight BackBufferFormat BackBufferCount Ширина и высота неактивной видеостраницы. Если программа работает в полноэкранном режиме, они должны соответствовать корректному режиму работы дисплея. В оконном режиме их можно выбирать любыми в пределах, которые позволяет ваша видеокарта Формат неактивной видеостраницы. Этот элемент - того же типа D3DFORMAT, который вы уже встречали в функции IDirect3D9::GetAdapterDisplayMode(). Он описан в таблице 2.1. В полноэкранном режиме BackBuf ferFormat устанавливает режим экрана. В оконном режиме его нужно установить в значение, соответствующее текущему режиму экрана. Microsoft утверждает, что это не обязательно, но зачем вам усложнять себе жизнь? Количество неактивных видеостраниц. Корректные значения - 0, 1, 2 и 3. Обычно вам будет нужна только одна неактивная видеостраница, но можно создать и больше, если вам это понадобится. Если указать 0 неактивных видеостраниц, DirectX все равно создаст одну
Имитация ЗР-графики с помощью DirectX 35 Элемент Описание MultiSampleType, MultiSampleQuality SwapEffect hDe vi се Window Windowed EnableAutoDepth- Stencil AutoDepthStencil- Format Flags FullScreen_ RefreshRatelnHz Presentation Interval Мультисэмплинг - это методика для выполнения сглаживания, имитации размытости быстро движущихся объектов и других эффектов Эти флаги описывают, как будет выполняться переключение видеостраниц. В большинстве игр используется значение D3DSWAPEFFECT_DISCARD. ОНО Сообщает Direct3D, что сохранять содержимое неактивной видеостраницы после ее переключения в активную не нужно. Позволив Direct3D быть несколько небрежным с неактивными видеостраницами, мы можем выиграть в производительности Дескриптор окна, в котором устройство будет выполнять рендеринг. Если он равен null, рендеринг будет выполняться в окне фокуса, указанном в функции IDirect3D9::CreateDevice() Если этот параметр установлен в true, приложение работает в окне, если в FALSE - приложение работает в полноэкранном режиме Установка этого параметра в true позволяет Direct3D распоряжаться буферами глубины за вас Тип буфера глубины, который вы хотите использовать, если вы установили в TRUE параметр EnableAutoDepthStencil Флаги, которые не поместились в другие параметры. Они нужны нечасто Частота обновления экрана в герцах. Эта частота может быть разной, но обычно мониторы обновляют изображение с частотой 75 Гц или выше. Присвоив этому параметру значение 0 или d3dpresent_rate_default, вы выберете заданную по умолчанию частоту обновления. В оконном режиме нужно использовать частоту по умолчанию, но в полноэкранном режиме вы можете выбрать любое корректное значение частоты Определяет, насколько быстро Direct3D предоставляет неактивную видеостраницу. Обычно этому параметру присваивается значение d3dpresent_interval_default. Это нужно делать в оконном режиме
36 Глава 2 Это практически все, что нужно сделать, чтобы Direct3D заработал. Вызов функции IDirect3D9: : CreateDevice () в программе будет, вероятно, выглядеть примерно так: // Создаем устройство if ( FAILED ( g_j>D3D-> CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, G_hMainWindow, D3DCREATE_HARDWARE_VERTEXPR0CESSING, Sd3dpp, SgjpDevice ) ) ) { return E_FAIL; } Этот вызов создает устройство, соответствующее адаптеру по умолчанию, использующее аппаратуру для выполнения растеризации, основное окно для вывода, выполняющее аппаратную обработку вершин и структуру d3dpp, которую мы только что рассмотрели. Функция IDirect3D9: : CreateDevice () поместит указатель на устройство в параметр g_j?Device. Подсказка Аппаратная обработка вершин намного быстрее программной, но некоторые (довольно старые) видеокарты ее не поддерживают. Если у вас из-за этого возникают проблемы, замените флаг d3dcreate_hardware_ VERTEXPROCESSING на D3DCREATE SOFTWARE VERTEXPROCESSIHG. ОСВОБОЖДЕНИЕ РЕСУРСОВ Вот и все! Direct3D официально инициализирован! С этого момента мы готовы начать моделировать и рисовать. Важно не забывать, что перед завершением выполнения программы нужно освободить интерфейсы в последовательности, обратной последовательности их инициализации. Сначала мы получили интерфейс IDirect3D9 в указателе g_pD3D, а затем — интерфейс IDirect3DDevice9 в указателе g_jpDevice. Освобождаются они в обратном порядке с помощью метода Release (): int Shutdown(void) { // Освобождаем указатель на IDirect3DDevice9. if ( g_pDevice ) < g_pDeviee->Release(); g_pDevice = 0; } // Освобождаем указатель ва IDirect3D9.
Имитация ЗР-графики с помощью DirectX 37 if ( g_pD3D ) { g_pD3D->Release(); g_pD3D = 0; > return S_OK; } Обратите внимание, что здесь указателям присваивается значение 0, а не NULL. И тот, и другой вариант правилен. Инициализация DirectX с помощью мастера DirectX AppWizard Использование мастера DirectX AppWizard очень упрощает запуск и подготовку Direct3D к использованию. Точнее говоря, мастер не только инициализирует Direct3D. Он инициализирует и все остальные компоненты DirectX. Последовательность работы с мастером DirectX AppWizard в разных версиях Visual Studio несколько различна. Чтобы помочь вам начать, я опишу работу с ним и в версии 6, и в версии 7. ИСПОЛЬЗОВАНИЕ МАСТЕРА DIRECTX APPWIZARD В VISUAL STUDIO 6 Вот как использовать мастер DirectX AppWizard в Visual Studio 6: 1. В меню File выберите пункт New. Visual Studio отобразит окно New. Если вкладка Projects не открыта, откройте ее с помощью мыши. 2. На странице New Projects показан список проектов, которые можно создать в Visual Studio. Выберите в этом списке пункт DirectX AppWizard. 3. В текстовом поле Project Name введите имя нового проекта. Пока назовем его просто InitDX. Щелкните на кнопке ОК. 4. AppWizard будет отображать последовательность диалоговых окон, в которых нужно задавать параметры DirectX-проекта. В первом диалоговом окне указывается общая информация о приложении. В верхней части диалогового окна нужно указать, приложение какого типа вы хотите создать. Все приложения, которые мы будем создавать в этой книге, будут однодокументными. Убедитесь, что выбрано однодокументное приложение, прежде чем продолжить.
38 Глава 2 5. Ни для одной программы из этой книги вам не понадобятся Di- rectMusic, DirectSound и DirectPlay. Создавая проект, сбросьте соответствующие флажки. 6. Пока не добавляйте в проект ничего. Убедитесь, что флажки добавления меню и доступа к реестру сброшены. Щелкните на кнопке Next. 7. В появившемся диалоговом окне Арр Wizard спросит вас, с какого приложения вы бы хотели начать. Выберите пустое приложение (Blank). Затем щелкните на кнопке Finish. Теперь Visual Studio сгенерирует набор файлов для вашего проекта. Позже мы разберемся, что делать с этими файлами. ИСПОЛЬЗОВАНИЕ МАСТЕРА DIRECTX APPWIZARD В VISUAL STUDIO 7 Интерфейс мастера DirectX Арр Wizard в Visual Studio 7 выглядит немного по-другому, но работает в основном так же, как и в Visual Studio 6. 1. В меню File выберите пункт New, а в открывшемся подменю - пункт Project. Visual Studio отобразит окно New Projects. В левой части этого окна есть список языков, которые поддерживает Visual Studio. Для каждого языка показан список типов проектов, которые можно создать. Щелкните на папке Visual C++ Projects, а затем выберите значок DirectX9 Visual C++ Wizard. 2. Введите имя проекта и щелкните на кнопке ОК. 3. В левой части появившегося довольно необычного диалогового окна содержится список вкладок. Справа от этого списка - пространство для отображения содержимого этих вкладок. Щелкните на ярлыке вкладки Project Settings. 4. В верхней части этой вкладки AppWizard спрашивает, какое приложение вы хотите создать. Все приложения, которые мы будем создавать в этой книге, будут однодокументными. Убедитесь, что выбран пункт Single document window. 5. Ни для одной программы из этой книги вам не понадобятся Di- rectMusic, DirectSound и DirectPlay. Создавая проект, сбросьте соответствующие флажки. 6. Пока не добавляйте в проект ничего. Убедитесь, что флажки Menu bar и Registry Access сброшены. 7. Щелкните на ярлыке вкладки Direct3D Options. На этой вкладке AppWizard спрашивает вас, с какого приложения вы бы хотели начать. Выберите пустое приложение - пункт Blank. 8. Щелкните на кнопке Finish, и работа с мастером закончена.
Имитация ЗР-графики с помощью DirectX 39 Если попробовать запустить только что созданное приложение, вы увидите, что оно отображает окно с синим фоном. В окне также выводится текст, сообщающий о работе приложения. Хотя мастер DirectX AppWizard весьма полезен, у сгенерированного им кода есть несколько недостатков. Во-первых, этот код весьма сложен. Если вы не знакомы с DirectX, разобраться в нем и понять, что этот код делает, будет непросто. К несчастью, вам придется это сделать. В сгенерированном коде есть несколько мест, которые нужно будет изменять при использовании этого кода. Потребуется немало времени, чтобы понять, где эти места и какие изменения нужно сделать. Вторая проблема, связанная с кодом, сгенерированным DirectX AppWizard, тесно перекликается с первой. Чтобы создать игру, нужно модифицировать несколько мест в этом коде. Они рассеяны в сгенерированном коде. Было бы удобнее, если бы можно было легко отделить ваш код от кода, сгенерированного мастером. Еще одна проблема - избыточность кода. Мастер ориентирован на генерацию кода для примеров, поставляемых с DirectX SDK. Он просто не создан для использования в качестве платформы для игр. В результате мастер вставляет в код возможности, которые не нужны играм. Например, он добавляет диалоговое окно, позволяющее настраивать DirectX. Дополнительный код можно удалить, но, поскольку он весьма сложен, часто бывает трудно понять, что можно выбросить, а что - нельзя. Кроме того, код, сгенерированный мастером, не очень быстро выполняется. Вероятно, вручную вы напишете более быстро выполняющийся код. Если вы знаток DirectX, то оптимизировать сгенерированный мастером код для вас не составит труда. Если вы не слишком хорошо разбираетесь в DirectX, это может быть очень сложно. И, наконец, сгенерированный мастером код не предназначен для использования как учебное пособие, хоть и снабжен изрядным количеством комментариев. В этой книге проще демонстрировать, о чем идет речь, короткими фрагментами кода, делающими именно то, что нужно. Инициализация DirectX с помощью платформы физического моделирования Чтобы избежать возни с кодом, сгенерированным мастером AppWizard, я написал аккуратную платформу, которая подготавливает Direct3D к работе. Она изолирует свой собственный код от вашего. Чтобы вызвать ваш код, платформа предоставляет стандартный набор функций для игры. Эти функции позволяют делать все, что нужно. Замечание Все функции платформы физического моделирования включены в пространство имен pmf ramework, поэтому нужно вставить оператор use namespace pmf ramework; в начало каждого файла . ерр в вашей игре.
40 Глава 2 Весь исходный код платформы можно найти на компакт-диске, поставляющемся с книгой. Код находится в папке Source\Chapter02\ PMFramework. КЛАСС ПРИЛОЖЕНИЯ DIRECT3D Платформа физического моделирования выполняет большую часть операций в классе d3d_app. Этот класс содержит всю информацию, нужную для инициализации программы в Windows и запуска DirectX. Сейчас класс d3d_app выполняет только простейшие операции. Точнее говоря, он запускает программу в оконном режиме и инициализирует Direct3D. В последующих главах мы модифицируем этот класс так, чтобы игра заработала в полноэкранном режиме. Кроме того, если вам понадобятся какие-то другие компоненты DirectX, например, DirectSound и DirectMusic, то придется расширить класс d3d_app. Сначала нужно добавить новые элементы данных, содержащие данные для инициализации этих компонентов. Затем нужно написать функции чтения и изменения значений этих компонентов. Определение класса d3d_app приведено в листинге 2.1. Листинг 2.1. Определение класса d3d_app 1 class d3d_app 2 { 3 private: 4 // Свойства приложения. 5 bool applnitialized; 6 7 // Свойства окна. 8 std: .-string windowTitle; 9 10 // Свойства D3D. 11 LPDIRECT3D9 direct3D; // Используется для 12 // создания D3DDevice 13 LPDIRECT3DDEVICE9 d3dDevice; // Устройство рендеринга 14 LPDIRECT3DVERTEXBUFFER9 vertexBuffer; // Буфер для хранения 15 // вершин 16 17 public: 18 d3d_app(); 19 bool InitApp( 20 std::string initialWindowTitle); 21 22 LPDIRECT3DDEVICE9 D3DRenderingDevice(void); 23 24 LPDIRECT3DVERTEXBUFFER9 D3DVertexBuffer(void); 25 void D3DVertexBuffer( 26 LPDIRECT3DVERTEXBOFFER9 vertexBufferPointer);
Имитация ЗР-графики с помощью DirectX 41 27 28 friend INT WINAPI AppMain( 29 HINSTANCE hlnst, 30 HINSTANCE, 31 LPSTR, 32 INT); 33 friend HRESULT InitD3D( 34 HWND hWnd); 35 friend VOID CleanupD3D(); 36 } ; В классе d3d_app есть private-элементы данных для приложения, окна и Direct3D. Например, в строке 5 листинга 2.1 объявлена переменная applnitialized. Значение этого элемента указывает, вызывалась ли функция InitAppO класса d3d_app. Эта переменная используется приложением. В строке 8 определена переменная WindowTitle, используемая окном приложения. В строках 11-14 определены переменные, используемые DirectX. В листинге 2.1 также содержатся прототипы public-методов класса d3d_app (они начинаются со строки 17). Конструктор класса d3d_app приводит все элементы данных в известные состояния. Это необходимо для правильного функционирования объекта d3d_app. Функция InitAppO передает начальные значения из игры элементам данных объекта d3d_app. В строке 22 определена функция D3DRenderingDevice () класса d3d_app. Эта функция получает указатель на устройство рендеринга Di- rect3D. Играм нужен этот указатель, чтобы использовать функциональность Direct3D. Большинство игр используют также вершинный буфер. В классе d3d_app есть переменная для хранения указателя на этот буфер и функции для чтения и изменения значения этой переменной. Прототипы этих функций содержатся в строках 24-26. В строках 28-35 класс d3d_app содержит объявления функций АррМа- in(), Init3D() и Cleanup3D(), дружественных этому классу. Когда я читал курс программирования на C++ в колледже, я настоятельно рекомендовал избегать использовать дружественные функции, поскольку они подавляют инкапсуляцию объектов. Однако в этом случае дружественные функции необходимы, чтобы предоставлять интерфейс между платформой физического моделирования и функциями, которые нужны для работы программы в Windows. Не нужно объявлять переменную типа d3d_app в вашей программе. Платформа уже делает это, объявляя переменную theApp. Эта переменная доступна во всех файлах .срр, в которые включен заголовочный файл PM3DApp.h. ФУНКЦИИ WINMAIN() И APPMAINQ В каждой программе для Windows должна присутствовать функция Win- Main (). Чтобы вам не пришлось писать ее самому, эта функция включена
42 Глава 2 в платформу. Однако функция WinMain () принадлежит к глобальному пространству имен. Поэтому ее трудно сделать дружественной к классу d3d_app, принадлежащему к пространству имен pmframework. Платформе нужно, чтобы ее основная функция была дружественной. Дабы разрешить это противоречие, функция WinMain () не делает почти ничего. Вся функциональность, которая обычно находится в ней, перенесена в функцию AppMain (). Функция WinMain () вызывает AppMain (), и функция AppMain () выполняет всю работу. Функция WinMain () приведена в листинге 2.2. Листинг 2.2. Функция WinMain() платформы физического моделирования 1 INT WINAPI WinMain( 2 HINSTANCE hlnstance, 3 HINSTANCE hPrevInstance, 4 LFSTR lpCmdLine, 5 INT nCmdShow) 6 { 7 return (pmframework::AppMain( 8 hlnstance,hPrevInstance,lpCmdLine,nCmdShow)); 9 } Как видите, функция WinMain () просто вызывает функцию AppMain (). WinMain () возвращает то значение, которое ей возвращает AppMain (). Функция AppMain () приведена в листинге 2.3. Листинг 2.3. Функция AppMain() платформы физического моделирования 1 INT WINAPI AppMain( 2 HINSTANCE hlnstance, 3 HINSTANCE hPrevInstance, 4 LPSTR lpCmdLine, 5 INT nCmdShow) 6 { 7 bool noError=true; 8 WNDCLASSEX we; 9 HWND hWnd; 10 11 noError=OnAppLoad(); 12 assert (theApp. applnitialized=true) ; 13 14 if (noError) 15 { 16 // Регистрируем класс окна 17 WNDCLASSEX tempWC = 18 { 19 sizeof(WNDCLASSEX),CS_CLASSDC,MsgProc,0L,0L,
Имитация ЗР-графики с помощью DirectX 43 20 GetModuleHandle(NULL),NULL,NULL,NULL,NULL, 21 D3DAPP_WINDOW_CLASS_NAME,NULL 22 }; 23 24 wc=tempWC; 25 if (RegisterCIassEx(&wc)=0) 26 { 28 return @); 29 } 30 } 31 32 if (noError) 33 { 34 // Создаем окно приложения 35 hWnd = CreateWindow( 36 D3DAPP_WINDOW_CLASS_NAME, 37 (LPCSTR)theApp.windowTitle.c_str(), 38 WS_OVERLAPPEDWINDOW, 39 100,100,256,256, 40 GetDesktopWindow(), 41 NULL,wc.hlnstance,NULL); 42 } 43 44 // Если окно создано... 45 if ((noError) && (hWnd'=NULL)) 46 { 47 // Если не удалось выполнить инициализацию... 48 if ('PreD3DInitialization()) 49 { 50 noError=false;; 51 } 52 } 53 54 // Инициализируем Direct3D 55 if ((noError) SS (SUCCEEDED(InitD3D(hWnd)))) 56 { 57 noError=PostD3DInitialization(); 58 } 59 60 // Инициализируем игру. 61 if (noError) 62 { 63 noError=GameInitialization<); 64 } 65 66 if (noError) 67 { 68 // Отображаем окно 69 ShowWindow(hWnd,SW SHOWDEFAULT);
44 Глава 2 70 UpdateWindow(hWnd); 71 72 // Запускаем цикл обработки сообщений 73 MSG msg; 74 ZeroMemory(Smsg,sizeof(msg)) ; 75 while(msg.message!=WM_QOIT) 76 { 77 if(PeekMessage(&msg,NULL,0U,0U,PM_REMOVE)) 78 { 79 TranslateMessage(Smsg); 80 DispatchMessage(Smsg); 81 } 82 else 83 { 84 UpdateFrame(); 85 Render(); 86 } 87 } 88 } 89 90 UnregisterClass(D3DAPP_WINDOW_CLASS_NAME,wc.hlnstance); 91 return 0; 92 } Функция AppMain () начинается с объявления используемых в ней переменных. Прежде чем делать что-то еще, она вызывает функцию OnAppLoad (). Прототип функции OnAppLoad () содержится в файле PM3DApp. h. Однако этой функции в платформе нет. Ее должна предоставить ваша игра, иначе скомпилировать ее будет невозможно. Вот минимальная версия функции OnAppLoad (). bool OnAppLoad() { // Следующий вызов ДОЛЖЕН присутствовать в этой функции. theApp.InitApp("test"); return (true); } Как видите, функция OnAppLoad () должна вызывать метод InitApp () класса d3d_app. В строке 12 функции InitApp () в листинге 2.3 содержится вызов макроса assert, который аварийно завершит выполнение программы, если функция OnAppLoad() не вызвала функцию InitApp (). Если функции OnAppLoad () удалось выполнить инициализацию, она возвращает значение TRUE. Убедившись, что приложение успешно проинициализировалось, функция AppMain () создает и регистрирует класс окна программы в строках 16-29 листинга 2.3. Эту операцию необходимо выполнять в каждой программе для Windows. Класс окна используется для создания окна в строках 35-41.
Имитация 3D-графики с помощью DirectX 45 Если Windows успешно создает окно программы, функция AppMain () вызывает функцию PreD3DInitialization(). PreD3DInitializati- on () - это еще одна функция, которая нужна платформе, но должна быть предоставлена вашей игрой. Если в игре не будет функции PreD3DIniti- alization(), ее не удастся скомпилировать. Эта функция предоставляет игре возможность выполнить все операции, которые ей нужно выполнить до инициализации Direct3D. Функция возвращает TRUE, если она выполнилась успешно, и FALSE, если нет. Если вашей игре не нужно выполнять никаких действий, прежде чем инициализировать Direct3D, функция PreD3DInitialization() должна просто возвращать TRUE. Далее функция AppMain () инициализирует Direct3D, вызывая функцию платформы InitD3D (). Эта функция рассматривается в следующем разделе. Если InitD3D() удается инициализировать Direct3D, она возвращает значение S_OK - стандартный код состояния в Windows. Если почему-либо инициализация не удается, InitD3D() возвращает значение E_FAIL - код ошибки в Windows. Проинициализировав Direct3D, функция AppMain () вызывает функцию PostD3DInitialization(). Эту функцию тоже должна предоставлять игра. В ней можно выполнять любые нужные игре действия. В строках 69-81 функция AppMain () выполняет стандартные для Windows-программ операции. Она отображает окно программы и запускает цикл обработки сообщений. Если сообщений, ожидающих обработки, нет, то функция AppMain () вызывает функцию UpdateFrame (). Именно в этой функции выполняются все операции, необходимые для рисования изображения. Собственно сцена рисуется вызовом функции Render () в строке 85 листинга 2.3. Эта функция вызывает функцию RenderFrame (), которую должна предоставлять игра. ИНИЦИАЛИЗАЦИЯ DIRECT3D Инициализацию Direct3D в платформе выполняет функция InitD3D (). Ее код приведен в листинге 2.4. Листинг 2.4. Функция lnitD3D() 1 HRESULT InitD3D(HWND hWnd) 2 { 3 HRESULT hr = S_OK; 4 D3DPRESENT_PARAMETERS d3dpp; 5 6 // Создаем объект Direct3D. 7 if((theApp.direct3D = 8 Direct3DCreate9(D3D_SDK_VERSION))—NOLL) 9 { 10 // Если объект не удалось создать... 11 hr = E_FAIL; 12 }
46 Глава 2 13 else 14 { 15 // Если объект Direct3D создан... 16 // Подготавливаем структуру, используемую при создании 17 // устройства D3DDevice 18 ZeroMemory(Sd3dpp,sizeof(d3dpp)); 19 d3dpp.Windowed = TRUE; 20 d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; 21 d3dpp.BackBufferFormat = D3DFMT_UNKNOWN; 22 } 23 24 // Создаем устройство D3DDevice 25 // Может ли устройство использовать HAL? 26 if ((hr==S_OK) && 21 (FAILED(theApp.direct3D->CreateDevice( 2 8 D3DADAPTER_DEFAULT,D3DDEVTYPE_HAL,hWnd, 29 D3DCREATE_HARDWARE_VERTEXPROCESSING, 30 Sd3dpp,StheApp.d3dDevice)))) 31 i 32 // Если нет, возможно, удастся использовать 33 // программную эмуляцию... 34 if{FAILED(theApp.direct3D->CreateDevice( 35 D3DADAPTER_DEFAULT, 36 D3DDEVTYPE_REF, 37 hWnd, 38 D3DCREATE_HARDWARE_VERTEXPROCESSING, 39 Sd3dpp, 40 StheApp.d3dDevice))) 41 { 42 // Если нет, увы... 43 hr = E_FAIL; 44 } 45 } 46 47 if (hr=S_OK) 48 { 49 /* Отключаем отсечение невидимых поверхностей, 50 чтобы видеть обе стороны треугольников.*/ 51 theApp.d3dDevice->SetRenderState( 52 D3DRS_CULLMODE, 53 D3DCULL_NONE); 54 55 /* Отключаем освещение Direct3D, поскольку 56 мы сами задаем цвета вершин.*/ 5 7 theApp.d3dDevice->SetRenderState(D3DRS_LIGHTING,FALSE); 58 } 59 60 return hr; 61 }
Имитация ЗР-графики с помощью DirectX 47 Объявив нужные ей переменные, функция lnitD3D() создает объект Direct3D. Если объект успешно создается, то в строках 18-21 листинга 2.4 задаются параметры экрана. Если вы хотите более тщательно настраивать эти параметры, добавьте private-элементы данных в класс d3d_app. Добавьте в функцию InitApp () этого класса параметры, которые позволят вам присваивать значения новым элементам данных при вызове функции InitApp (). Затем перепишите код в строках 18-21 так, чтобы в структуру помещались значения этих элементов данных, и используйте эти значения для задания параметров экрана. Затем функция InitD3D {) пытается создать устройство Direct3D. Сначала она пытается использовать HAL в строках 27-30. Если это не удается, то она пытается использовать программную эмуляцию в строках 34-40. Платформа делает несколько попыток инициализации, прежде чем сдаться. Это важно. Direct3D позволяет использовать аппаратные вершинные процессоры. Если в компьютере пользователя установлена видеокарта, возраст которой более трек лет, она, вероятно, не содержит вершинных процессоров. Если бы платформа не пыталась использовать программную обработку вершин, многие пользователи не смогли бы играть в вашу игру. Создав устройство Direct3D, функция InitD3D () отключает отсечение невидимых поверхностей. Отсечение невидимых поверхностей означает, что Direct3D игнорирует полигоны, нормальные векторы которых указывают в направлении от точки наблюдения. Хотя отсечение и ускоряет обработку, оно может помешать получить нужное изображение. Это еще один момент, из-за которого вы, возможно, захотите добавить дополнительные возможности настройки. Их можно добавить тем же описанным выше способом, что и возможности настройки экрана, то есть добавить private-элементы данных в класс d3d_app, проинициализировать их в функции InitApp () и обратиться к этим элементам данных в строках 51-53. В строке 57 функция InitD3D () отключает возможности Direct3D по моделированию освещения. Это делается потому, что в примерах программ из нескольких последующих глав эти возможности не используются. Позже я продемонстрирую процесс добавления возможностей настройки. Мы создадим возможность включать и выключать возможности моделирования освещения при вызовах функции InitApp (). ИНИЦИАЛИЗАЦИЯ ИГРЫ Если вы посмотрите на функцию AppMain () (листинг 2.3), то увидите, что она вызывает функцию InitD3D (), а затем позволяет игре выполнить дополнительную инициализацию. Для этого вызывается функция PostD3DI- nitializationO . Эту функцию должна предоставлять ваша игра. Функция PostD3DInitialization() может выполнять все, что вы посчитаете нужным. Например, она может выводить заставку с названием и эмблемой издателя. Эта функция также может отображать основное меню игры, чтобы позволить игроку начать новую игру или загрузить ранее сохраненную.
48 Глава 2 Когда выполнение функции PostD3DInitialization() завершается, она должна вернуть значение TRUE при успешном выполнении или FALSE при ошибке. Если вы не хотите, чтобы эта функция выполняла какие бы то ни было действия, сделайте так, чтобы она просто возвращала значение TRUE. После вызова функции PostD3DInitialization {) функция АррМа- in () вызывает функцию Gamelnitialization (). Эта функция требуется платформе, но предоставить ее должна ваша игра. В большей части этой книги мы будем использовать функцию Gamelnitialization () для определения объектов и сцен, отображаемых примерами программ. ОБРАБОТКА СООБЩЕНИЙ И ДЕЙСТВИЙ ПОЛЬЗОВАТЕЛЯ Все программы в Windows получают сообщения и обрабатывают их в специальных процедурах. Сообщения могут означать что угодно - от нажатия клавиши до указания перерисовать экран. Платформа содержит простую функцию обработки сообщений, которая называется MsgProc(). Ее код приведен в листинге 2.5. Листинг 2.5. Функция MsgProcQ 1 LRESULT WINAPI MsgProc( 2 HWND hWnd, 3 UINT msg, 4 WPARAM wParam, 5 LPARAM 1Param) 6 { 7 if ('HandleMessage(hWnd,msg,wParam,lParam)) 8 { 9 switch(msg) 10 { 11 case WM_DESTROY: 12 // Освобождаем использовавшиеся ресурсы. 13 GameCleanup(); 14 // Освобождаем ресурсы Direct3D 15 CleanupD3D{); 16 PostQuitMessage(O); 17 return 0; 18 } 19 } 20 return DefWindowProc(hWnd,msg,wParam,lParam); 21 } Это простейшая версия функции обработки сообщений. Она начинается с вызова функции HandleMessage {) в строке 7 листинга 2.5. Это еще одна функция, которую должна предоставлять ваша игра. Если ваша
Имитация ЗР-графики с помощью DirectX 49 игра обрабатывает сообщение, то функция HandleMessage () должна возвращать значение TRUE, если нет - FALSE. Если сообщение не обработано, оно будет обрабатываться функцией MsgProc () . Подсказка За дополнительной информацией обращайтесь к теме «Window Messages» в документации к SDK для платформы Windows. В данной версии функция MsgProc () обрабатывает только одно сообщение — WM_DESTROY. Если она получает это сообщение, то вызывает функцию GameCleanup () в строке 13. Функцию GameCleanup () тоже должна предоставлять ваша игра. Затем функция MsgProc () вызывает функцию платформы CleanupD3D (), которая освобождает задействованные ресурсы Direct3D. Остальные сообщения в строке 20 передаются Windows для обработки. ОБНОВЛЕНИЕ И ПРОРИСОВКА КАДРОВ Чтобы действительно нарисовать что-то на экране, игра должна предоставлять две функции. Первая из этих функций - UpdateFrame (). В этой функции с помощью Direct3D выполняются все операции, которые нужно выполнить, прежде чем прорисовывать очередной кадр анимации. Именно в ней и будет работать вся физика, которую мы будем изучать в последующих главах. Вторая функция, которую должна предоставить ваша игра, - Ren- derFrame(). Эта функция будет непосредственно рисовать кадр. Замечание Вашей игре не нужно вызывать функции Direct3D BeginScene () и EndSce- ne (). Платформа сделает это за вас. Все, что нужно сделать вам, - прорисовать нужную вам геометрию. Обычно это значит, что нужно прорисовать содержимое вершинного буфера. ОЧИСТКА Как говорилось выше, при получении сообщения WM_DESTROY платформа вызывает функцию GameCleanup (). Эту функцию должна предоставлять ваша игра. После завершения ее выполнения платформа вызовет функцию Cleanup3D (), код которой приведен в листинге 2.6. Функция Cleanup3D() освобождает объекты DirectX, созданные платформой. Как уже упоминалось выше, эти объекты освобождаются в последовательности, обратной последовательности их создания.
50 Глава 2 Листинг 2.6. Функция Cleanup3D() VOID CleanupD3D() { // Освобождаем вершинный буфер, if(theApp.vertexBuffer != NULL) TheApp.vertexBuffer->Release(); // Освобождаем устройство рендеринга, if(theApp.D3DRenderingDevice() != NULL) theApp.D3DRenderingDevice()->Release(); // Освобождаем объект Direct3D. if(theApp.direct3D != NULL) theApp.direct3D->Release(); Замечание На компакт-диске есть файл, содержащий все требующиеся платформе функции. Эти функции в файле пусты и ничего не делают. Файл называется FrameFns.cpp и находится в папке Source\Chapter02. Этот файл можно использовать, чтобы скомпилировать платформу и с помощью отладчика проследить за ее работой. Итоги DirectX - мощный инструмент для написания игр и программ, работающих с ЗО-графикой. С помощью мастера DirectX AppWizard или платформы физического моделирования можно избежать необходимости выполнять множество трудоемких операций по инициализации DirectX и освобождению ресурсов. Однако чтобы эффективно использовать DirectX, нужно понимать основы ее архитектуры и назначение ее компонентов. Теперь, кратко познакомившись с DirectX - основным инструментом, который мы будем использовать для написания игр и работы с ЗО-графикой, можно перейти к изучению основных инструментов, которые мы будем применять для физического моделирования. Этим инструментам посвящена глава 3.
Глава 3 Математические инструменты В этой главе вы познакомитесь с основными математическими инструментами, используемыми для физического моделирования и написания программ, работающих с ЗБ-графикой. Для написания таких программ используется евклидова геометрия, которая на самом деле отнюдь не так сложна, как вы, возможно, думаете. Большую часть того, что нам нужно, можно сделать, обладая базовыми знаниями о треугольниках. А эти знания весьма просты. Для написания программ работы с ЗБ-графикой нужно также понимать систему декартовых координат. Также нужно разбираться в векторах. Эта глава заканчивается обсуждением матриц, которые понадобятся нам при просчете перспективы в сценах и анимировании ЗБ-объектов. Геометрия треугольников Для программистов, работающих с ЗБ-объектами, треугольники - один из самых важных инструментов. Возможно, это покажется вам странным, но это так. Например, можно определить, плоская ли поверхность, нарисовав на ней треугольник. Если поверхность плоская, то сумма внутренних углов любого нарисованного на ней треугольника будет равна 180°. Сумма внутренних углов треугольника на поверхности сферы всегда будет больше 180°. Это видно из рисунка 3.1. Рис. 3.1. Треугольник на плоскости и на сфере
52 Глава 3 Треугольник образуют три пересекающиеся прямые. Углы в треугольниках измеряются в градусах или радианах. Сумма углов треугольника на плоскости равна 180°. Если один из углов треугольника равен 90°, этот треугольник называется прямоугольным. У прямоугольных треугольников есть удобное свойство, описываемое теоремой Пифагора. Предположим, что у нас есть прямоугольный треугольник, стороны которого обозначены так же, как и на рисунке 3.2. Тогда для этого треугольника теорему Пифагора можно записать в виде: а2=Ь2+с2 Ь Рис. 3.2. Прямоугольный треугольник Эта формула означает, что квадрат длины самой длинной стороны прямоугольного треугольника (эта сторона называется гипотенузой) равен сумме квадратов длин двух других сторон. Поэтому, если мы знаем длину этих двух сторон и хотим найти длину гипотенузы, это можно сделать, преобразовав теорему Пифагора к виду: a=Vb2+c2 Попробуем воспользоваться этой формулой на практике. Предположим, что нам нужно найти длину гипотенузы прямоугольного треугольника, длины двух других сторон которого равны 3 и 4. Длину его гипотенузы мы получим так: ^л/з2^2 а=л/9+16 a=V25 а=5
Математические инструменты 53 Таким образом, длина гипотенузы равна 5. Как вы скоро поймете, этот же метод можно использовать, чтобы находить расстояние между двумя точками в двумерных и трехмерных системах координат. Двумерные системы координат Реальный мир существует не в какой-то фиксированной системе координат. Во вселенной нет линеек с делениями. Любая точка, прямая, вектор или матрица существуют, не будучи привязанными к конкретной системе координат. Физические законы работают независимо от того, какие системы координат вы используете. Вы используете координаты, рассматривая количественные характеристики объектов. Другими словами, координаты позволяют нам измерять объекты и присваивать им численные характеристики. Например, координаты позволяют нам найти расстояние до камня или высоту небоскреба. Предположим, что мы говорим о достопримечательностях Канзас-сити в штате Миссури. Местоположение музея Нельсона-Аткинса можно определить, сообщив, что он находится по адресу 4525 Оук-стрит, а можно сообщить, что он находится в точке с координатами 39.045° северной широты и 94.581° западной долготы по показаниям GPS (Global Positioning System - система глобального позиционирования). Музей находится в одном и том же месте, мы просто использовали разные системы координат, чтобы указать его местоположение. Для указания местоположения объекта на плоскости или на приближенно плоской поверхности (например, части поверхности большой сферы) нужны два числа. Есть несколько способов задания координат на плоскости. На рисунке 3.3 показаны наиболее распространенные способы. (X, Y) (R, в) • *, * - „ ~-^€\ Декартовы •--(■--' Полярные Эллиптические Рис. 3.3. Наиболее распространенные способы задания координат на плоскостях
54 Глава 3 В играх чаще всего используются декартовы координаты. Декартова система координат использует две перпендикулярные друг к другу оси координат — х и у. Точка задается значением по оси х и значением по оси у. Точку можно записывать в виде упорядоченной пары чисел (х, у). Например, чтобы найти точку C, 2) в декартовой системе координат, нужно отсчитать три деления по оси х и два деления по оси у, как показано на рисунке 3.4. C, 2) I t I ► х Рис. 3.4. Декартова система координат Трехмерные и четырехмерные системы координат Часто нам бывает нужно больше двух координат. Например, пространство в нашей вселенной как минимум трехмерное. Больше трех измерений представить весьма сложно, а время легко отличить от пространства. В этом случае координаты точки можно определять с помощью трехмерной декартовой системы координат. Любую точку в пространстве можно указать тремя числами в виде (х, у, z), как показано на рисунке 3.5. Идея кажется весьма простой, но будьте осторожны. В трехмерных декартовых системах координат есть неоднозначность. В каком направлении указывает ось z? Если ось у вертикальна, а ось х - горизонтальна, то растут ли координаты z по мере приближения к вам (выходя из страницы) или по мере удаления от вас (уходя в страницу)? Ответ на этот вопрос не совсем произволен. Физики, математики и инженеры приняли в качестве стандарта правостороннюю систему координат более 100 лет назад. Именно она изображена на рисунке 3.5. Эта система координат используется во всех книгах по физике, поэтому я буду использовать ее в дискуссиях о физике и математике в этой книге.
Математические инструменты 55 Рис. 3.5. Трехмерная правосторонняя декартова система координат Замечание Возможно, вас интересует, почему системы координат называются «правосторонними» и «левосторонними». Попробуйте вытянуть руку в направлении оси х в системе координат и согните пальцы в направлении оси у. Отогните большой палец. Если это ваша правая рука, большой палец будет указывать в вашем направлении. Поэтому в правосторонней системе координат ось z указывает на вас (из страницы), если проделать тот же опыт для левой руки, большой палец будет указывать в направлении от вас. Поэтому в левосторонней системе координат ось z указывает от вас (в страницу). Если правосторонняя система координат была стандартом в течение столетия, как вы думаете, какая система координат используется в Di- rect3D? Увы! Левосторонняя. Чтобы увидеть разницу между этими двумя системами, посмотрите на рисунок 3.6. Правосторонняя Левосторонняя Рис. 3.6. Правосторонняя и левосторонняя система координат
56 Глава 3 Предупреждение Из-за этой путаницы многие авторы книг по программированию используют разные координатные системы, не замечая этого. Будьте внимательны! Это замечание стоит повторить. В книгах по физике и математике обычно используется правосторонняя система трехмерных координат. По причинам, связанным с устройством аппаратуры, в большинстве систем компьютерной графики используется левосторонняя система. Кроме того, учтите, что ось у не обязана всегда быть вертикальной. Можно выбрать правостороннюю систему координат и развернуть ее так, что ось z будет направлена вертикально вверх, как на рисунке 3.7. Такая система координат весьма часто используется в книгах по физике и математике. Рис. 3.7. Правосторонняя система координат с вертикально направленной осью z А как же четвертое измерение? Ну, его довольно сложно нарисовать (вообще-то, даже три измерения довольно сложно нарисовать на ПЛОСКОМ листе бумаги!). Однако с математической точки зрения добавить еще одно или несколько измерений совсем просто. В четырехмерной системе координат точка описывается четырьмя числами. Последнее измерение обычно обозначают w, и точка будет описываться (х, у, z, w). Единицы измерения Числа, определяющие координаты точек в системе координат - это расстояния, измеренные в определенных единицах. Пространство, с которым мы будем работать, обычно представляет собой привычное нам пространство, и единицами будут привычные нам единицы расстояния, например, метры или километры. В этой книге в основном используются метрические единицы. Хотя тип используемых единиц измерения не слишком важен для компьютерных
Математические инструменты 57 игр, почти во всех книгах по физике (и почти везде за пределами США) используются метрические единицы, поэтому будет удобнее использовать их, создавая физические модели. Метрические единицы - это часть Международной системы единиц СИ (SI - Systeme International). В таблице 3.1 перечислены основные метрические единицы, а в таблице 3.2 - некоторые коэффициенты преобразования английских единиц измерения в метрические. Таблица 3.1. Основные метрические единицы измерения Величина Базовая единица измерения Производные единицы Расстояние Масса Время Температура метр (м) килограмм (кг) секунда (с) кельвин (К) 1 километр (км) = 1000 м 1 м = 100 сантиметров (см) 1кг = 1000 граммов (г) 1 г = 1000 миллиграммов (мг) Система, использующая эти единицы в качестве базовых, называется МКС (Метр-Килограмм-Секунда) Таблица 3.2. Коэффициенты преобразования между метрическими и английскими единицами измерения Величина Преобразования Длина Масса Давление Температура 1 км = 0.6214 мили 1 миля = 1.6 км 1 метр =1.1 ярда = 3.28 фута 1 фут =12 дюймов = 30.48 см 1 кг = 0.06852 пуда 1 ньютон (Н) = 0.225 фунта 1 фунт = 4.45 ньютона 1 атмосфера (атм)* = 101 килопаскаль (кПа) 101 000 Н/м2 = 14.7 фунта/кв. дюйм 273 К = 0° С = 32° F 373 К= 100° С = 212° F 1 атмосфера есть атмосферное давление на уровне моря
58 Глава 3 Физики используют метрическую систему, поскольку при изучении английской системы может показаться, что ее составлял накачанный наркотиками мутант. 12 дюймов в футе; 5 280 футов в миле; 16 унций в фунте. В тесте, который автору довелось сдавать в колледже, ответ нужно было давать в фэрлонгах. После теста мы спросили у профессора, чему равен фэрлонг, и получили ответ: он равен десяти длинам поля для игры в крикет. М-да... Метрическая система куда как проще. 1000 метров в километре; 1000 миллиметров в метре; 1000 грамм в килограмме; 1000 миллиграмм в грамме - все ясно. Как получилось, что английская система так сложна? Эта система складывалась в течение долгого времени, и происхождение используемых в ней единиц разное. Например, фут равен средней длине ступни. С другой стороны, метрическая система СИ была спроектирована, а не сформировалась исторически. Она специально спроектирована так, чтобы ее было просто использовать. Преобразования в СИ просты и не требуют долгих расчетов. Поскольку преобразования требуют меньше расчетов, лучше использовать в играх систему СИ. Это позволяет снизить загрузку процессора бессмысленными вычислениями. Приставки к единицам системы СИ обладают четко определенным смыслом. Например, приставка кило всегда означает 1000. Поэтому километр - это 1000 метров, а килограмм - 1000 граммов. Наиболее часто используемые приставки перечислены в таблице 3.3. Таблица 3.3. Приставки в метрической системе Приставка Тера (Т) Гига (Г) Мега (М) кило (к) санти (с) милли(м) микро (мк или ц) нано (н) пико (п) Численный эквивалент 1012 109 106 103 Ю-2 ю-3 10 ю-9 ю-12 Все производные единицы измерения и константы в этой книге основываются на системе МКС, в которой базовыми единицами являются метр, килограмм и секунда. В таблице 3.4 перечислены некоторые производные
Математические инструменты 59 единицы измерения системы МКС. Если вам нужно работать с производными единицами измерения, используйте их. Поскольку константы будут указываться в системе МКС, другие величины тоже нужно будет представлять в этой системе, прежде чем использовать их для расчетов. Таблица 3.4. Производные единицы измерения системы МКС Величина Единица измерения Преобразование Сила Энергия Мощность Частота Давление ньютон (Н) джоуль (Дж) ватт (Вт) герц (Гц) паскаль (Па) 1 Н = 1 кг • м / с2 1 Дж= 1 Н • м 1 Вт = 1 Дж / с 1 Гц = 1 цикл / с 1 Па = 1 Н / м2 Единицы измерения весьма полезны - пользуйтесь ими. Одна из самых распространенных ошибок в физических и инженерных расчетах - использование неправильных единиц измерений. Если результат кажется неправдоподобным (танки не двигаются со скоростью 500 000 м/с), проверьте, правильно ли вы использовали единицы измерения. Кроме того, заметьте, что можно умножать и сокращать единицы измерения в формулах, как и обычные алгебраические переменные. Например, пройденное расстояние (d) есть постоянная скорость (v) умноженная на время (t). Можно убедиться в правильности этой формулы, записав в нее размерности величин: d = v • t Этот прием называется проверкой размерности. Если использованы верные единицы измерения, то, скорее всего, и ответ получен правильный: м = (м / с) • с Векторы В физике есть величины, характеризуемые единственным значением: масса камня, температура пламени, прошедшее время. Эти величины не изменятся, какую бы координатную систему вы ни выбрали. Они называются скалярами (scalars) и представляются просто числами. Обычно скаляры обозначаются строчными буквами. У других величин есть не только значение, но и направление. Такие величины называются векторами (vectors). У всех векторов есть величина и направление. Можете воспринимать вектор как стрелку определенной длины, как показано на рисунке 3.8.
60 Глава 3 \ \ 1 Рис. 3.8. Случайные векторы В книгах векторы обычно обозначаются жирными строчными буквами, например, v. Чтобы записать вектор на бумаге, поставьте над буквой стрелочку, например,v. Хотя вектор существует независимо от какой-то конкретной системы координат, в любой двумерной системе координат вектор можно представить в виде пары чисел - компонентов вектора. Компоненты двумерного вектора v в некоторой координатной системе можно записать в виде (vx, v ). Восприняв вектор в виде стрелки, указывающей из начала координат - точки с координатами @, 0) - в точку, координаты которой заданы компонентами вектора, мы получим графическое представление длины и направления этого вектора. Если вам трудно уяснить только что сказанное, взгляните на рисунок 3.9. Представляя себе вектор таким образом, помните, что у вектора есть только длина и направление, но нет местоположения. Вектор можно размещать там, где это удобно. ТУ I—I 1—I 1—Н -6 -5 -4 -3 -2 -1 1 2 3 4 5 6 i—i—i—i—ix B, -5) Рис. 3.9. Вектор B, -5) У вас есть полное право спросить, зачем нужны векторы в физике. Ответ можно понять из простого примера. Представьте себе цилиндрический зонд, висящий в космосе. Представьте, что он висит неподвижно.
Математические инструменты 61 Он может начать двигаться, только если его толкнет какая-нибудь сила. У силы есть не только величина, но и направление. Такую силу проще всего представить в виде вектора. Чтобы использовать векторы в ЗБ-графике и физике, нужно познакомиться с операциями над векторами с разным количеством измерений — от одного до четырех. Фокус в том, что если операции над векторами записывать в векторной форме, а не в виде операций над компонентами векторов, то форма записи будет одной и той же при любом количестве измерений у вектора и при любой координатной системе. Есть одно исключение из только что сказанного - векторное произведение двух векторов существует только в трехмерных координатах. Скоро мы рассмотрим его подробнее. Одномерные векторы - это просто скаляры. Мы уже разобрались, что такое двумерные векторы. Компоненты трехмерного вектора можно записать в виде (vx, vy, vz), а четырехмерного - (vx, vy) vz, vw). Представить себе трехмерный вектор можно как стрелку из начала трехмерной системы координат в точку с координатами, равными его компонентам. Такое представление позволит определить длину и направление вектора. Представить себе четырехмерный вектор будет сложно. Замечание Возможно, вы удивитесь, зачем вам нужны четырехмерные векторы, если вы не работаете с теорией относительности. Оказывается, четырехмерные векторы очень важны в ЗР-графике. Длина или магнитуда (magnitude) вектора - это скалярная величина, называемая также нормой (norm) вектора. Норму вектора часто записывают в виде вектора в прямых скобках, например, |v|. Физики обычно записывают норму просто как скаляр с тем же буквенным обозначением, что и вектор, например, норма вектора v обозначается v. Обратите внимание, что иногда норма обозначается прямыми двойными скобками, например, ||v||, поскольку прямые одинарные скобки могут обозначать абсолютное значение числа. Но, на мой взгляд, это слишком. Поскольку векторы обозначаются жирными буквами, можно отличить норму вектора |v| от абсолютного значения числа |v|. Замечание В применении к векторам термины длина, норма и магнитуда имеют одно и то же значение. Хороший пример вектора - вектор смещения (или перемещения). Этот вектор указывает из одной точки в пространстве в другую. В системе координат, в которой началом является точка начала этого вектора, его компоненты будут такими же, что и координаты точки, в которую он указывает, как на рисунке 3.10.
62 Глава 3 Точка А Рис- 3.10. Вектор смещения Нормой вектора смещения будет расстояние между точками, которые этот вектор соединяет. Реализация векторов в коде программ Моделирование физики в ЗБ-играх и графических программах сводится в основном к выполнению определенных математических операций. Поэтому прежде чем двигаться дальше, мы создадим библиотеку математических функций, позволяющую выполнять физическое моделирование. Пока все, что мы сможем поместить в эту библиотеку, - это определения функций для работы с двумерными и трехмерными векторами. В листинге 3.1 приведено определение класса, реализующего двумерные векторы. Листинг 3.1. Класс vector_2d 1 class vector_2d 2 { 3 private: 4 scalar x,y; 5 6 public: 7 vector_2d(void); 8 vector_2d(scalar xComponent,scalar yComponent); 9 vector_2d(const vector_2d firightOperand); 10 11 void X(scalar xComponent); 12 scalar X(void); 13 14 void Y(scalar yComponent); 15 scalar Y(void); 16 17 void SetXY(scalar xComponent,scalar yComponent); 18 void GetXY(scalar SxComponent,scalar SyComponent); 19 20 vector_2d Soperator =(const vector_2d &rightOperand); 21 }; Замечание Вы, конечно, заметили, что в этой книге строки в листингах пронумерованы. Разумеется, в файлах исходного кода строки не пронумерованы. Номера строк в листингах проставлены для того, чтобы облегчить разъяснение кода.
Математические инструменты 63 В листинге приведено определение очень простого класса для представления двумерных векторов. Каждый вектор будет состоять из двух чисел, хранящихся в private-элементах х и у. На данный момент в классе vector_2d есть девять методов. Скоро их количество увеличится. Пока список методов начинается с двух конструкторов. За ними идет метод записи значения компонента х вектора. Следующий метод позволяет считывать это значение. Аналогичная пара методов записывает и считывает значение компонента у вектора. Кроме того, в классе есть методы для записи и чтения обоих компонентов одновременно. Завершает определение класса перегруженный оператор присваивания, позволяющий присваивать один объект класса vector_2d другому такому объекту этого класса. Поскольку методы класса весьма просты, их код в книге не приводится. Если вы хотите посмотреть этот код, он находится на компакт-диске в папке Source\Chapter03, в файле PMMathLibVl. h. В листинге 3.2 приводится код определения класса для трехмерных векторов. Листинг 3.2. Класс vector_3d 1 class vector_3d 2 { 3 private: 4 scalar x,y,z; 5 6 public: 7 vector_3d(void); 8 vector_3d(scalar xComponent,scalar yComponent, 9 scalar zComponent); 10 vector_3d(const vector_3d SrightOperand); 11 12 void X(scalar xComponent); 13 scalar X(void); 14 15 void Y(scalar yComponent); 16 scalar Y(void); 17 18 void Z(scalar yComponent); 19 scalar Z(void); 20 21 void SetXYZ(scalar xComponent,scalar yComponent, 22 scalar zComponent); 23 void GetXYZ(scalar SxComponent,scalar &yComponent, 24 scalar (zComponent); 25 26 vector_3d (operator =(const vector_3d SrightOperand); 27 );
64 Глава 3 Как и класс vector_2d, класс vector_3d использует для хранения компонентов векторов private-элементы данных. Методы класса vec- tor_3d практически идентичны таким же векторам в классе vector_2d. Отличие этих методов в том, что в классе vector_3d они поддерживают работу с компонентом z. Кроме того, в этом классе есть дополнительная пара функций Z (). Первая функция этой пары записывает значение в компонент z, а вторая - считывает значение компонента. Создавая реальную математическую библиотеку, вы, вероятно, создавали бы классы для векторов по-другому. Вероятно, вы бы использовали шаблоны или наследование (или и то, и другое), чтобы создать более эффективные классы для работы с векторами. Но в целях создания предельно простого и понятного кода я избегал применения сложных приемов в этих классах. Классы векторов довольно просты. Однако они формируют хорошую основу для продвижения вперед. В нескольких последующих разделах мы рассмотрим стандартные операции с векторами. Рассматривая каждую такую операцию, мы будем добавлять в классы возможность ее выполнения. СЛОЖЕНИЕ И ВЫЧИТАНИЕ ВЕКТОРОВ Вектор можно прибавить к другому вектору, получив в результате новый вектор. Эта операция записывается в виде а + Ь. Если векторы представлять стрелками, то сложение векторов можно изобразить следующим образом. Поместим исходную точку вектора b в конечную точку вектора а, как показано на рисунке 3.11. Тогда вектор а + b есть вектор, исходная точка которого совпадает с исходной точкой вектора а, а конечная - с конечной точкой вектора Ь. (а + Ь) рис_ з.11. Сложение векторов Не имеет значения, в каком порядке идут прибавляемые векторы, то есть а + b = b + а. Как видно из рисунка 3.12, обе операции дают один и тот же результат. Это свойство называется коммутативностью операции сложения векторов. Рис. 3.12. Коммутативность операции сложения векторов При сложении векторов в декартовой системе координат компонент х получаемого вектора есть сумма компонентов х исходных векторов, а компонент у есть сумма компонентов у исходных векторов. Поэтому,
Математические инструменты 65 если компоненты вектора а - (ах, а^), а компоненты вектора b - (bx, b ) в той же координатной системе, то компоненты вектора а + b есть (ах + Ьх, s + V Расширить понятие сложения векторов на трехмерные и четырехмерные векторы просто. Соответствующие компоненты векторов попарно складываются. Поэтому для трехмерных векторов а + b есть (а^ + Ьх, а^ + by, az + bz), а для четырехмерных а + b есть (а^ + bx, s^ + by, az + bz, a^ + bw). J\. K3.K ЯС6 вычитание? Это операция, обратная сложению. То есть, (а - b) + b = a Это уравнение связывает вычитание и сложение. Можно использовать для изображения вычитания тот же чертеж, поскольку вычитание связано со сложением! Сравните это выражение со следующим: а - (а - b) = b Посмотрите на рисунок 3.13. Вы увидите, что результатом вычитания одного вектора из другого будет вектор, начальная точка которого совпадает с начальной точкой первого, а конечная - с начальной точкой второго. Рис. 3.13. Вычитание векторов В компонентной форме записать вычитание не составляет труда. Если компоненты вектора а - (ах, ау), а компоненты вектора b - (bx, b ), то вектор а — b будет состоять из компонентов (ах - Ьх, а — b ). Операция вычитания векторов не обладает коммутативностью. Вектор b — а указывает в направлении, противоположном направлению вектора а — Ь. Классы векторов несложно расширить так, чтобы они реализовывали операции сложения и вычитания векторов. Первый шаг - добавить в определения классов прототипы методов сложения и вычитания. Эти прототипы приведены в листинге 3.3. Листинг 3.3. Прототипы для методов сложения и вычитания векторов 1 // прототипы методов для класса vector_2d 2 vector_2d operator +(vector_2d SrightOperand); 3 vector_2d operator -(vector_2d SrightOperand); 4 5 // прототипы методов для класса vector_3d 6 vector_3d operator +(vector_3d SrightOperand); 7 vector_3d operator -(vector_3d SrightOperand); Приведенные ранее в этом разделе формулы показывают, что для сложения и вычитания векторов нужно складывать и вычитать их соответствующие компоненты. Код методов, выполняющих сложение и вычитание, приведен в листинге 3.4.
66 Глава 3 Листинг 3.4. Методы для сложения и вычитания векторов 1 // методы для класса vector_2d 2 inline vector_2d vector_2d::operator +(vector_2d SrightOperand) 3 { 4 return(vector_2d(x+rightOperand.x,y+rightOperand.у)); 5 } 6 7 inline vector_2d vector_2d::operator -(vector_2d SrightOperand) 8 { 9 return(vector_2d(x-rightOperand.x,y-rightOperand. y)) ; 10 > 11 12 // методы для класса vector_3d 13 inline vector_3d vector_3d: .-operator + (vector_3d SrightOperand) 14 { 15 return(vector_3d(x+rightOperand.x, y+rightOperand.у, 16 z+rightOperand.z)); 17 } 18 19 inline vector_3d vector_3d::operator -(vector_3d SrightOperand) 20 { 21 return(vector_3d{x-rightOperand.x, y-rightOperand.y, 22 z-rightOperand.z)); 23 } Все эти методы работают практически одинаково. Они создают безымянные временные переменные, вызывая конструкторы своих классов. Затем в списках параметров выполняется сложение или вычитание. Такой подход позволяет добиться максимальной эффективности, поскольку большая часть компиляторов C++ устранит безымянную переменную, заменив ее простым возвратом значения, получаемого в списках параметров конструкторов, причем сам конструктор вызываться не будет. Еще одна причина эффективности этих методов в том, что они встраиваемые. Лично мне не нравятся определения классов, забитые кодом методов этих классов. Я помещаю в определения только типы элементов данных и прототипы функций. Однако мне не хочется терять эффективность, свойственную встраиваемым функциям, поэтому в заголовки функций добавлено ключевое слово inline везде, где это возможно. Если вы работаете в Visual C++, то использовать inline не так уж необходимо. Visual C++ автоматически сделает встраиваемыми все функции, какие сможет. У него есть собственный алгоритм определения того, какие функции встроить. Когда вы компилируете версию программы для распространения, Visual C++ автоматически применит этот алгоритм. Если вы компилируете версию для отладки, то функции не делаются встраиваемыми, чтобы их выполнение можно было пошагово отслеживать с помощью отладчика.
Математические инструменты 67 Версии классов vector_2d и vector_3d с операторами сложения и вычитания векторов можно посмотреть в файле PMMathLibV2 . h. Этот файл находится на компакт-диске в папке Source\Chapter03. Помимо простых операторов сложения и вычитания векторов нам пригодятся и операторы += и -=. Замечание В языке C++ запись u += v эквивалентна записи и = и + v, а запись u -= v эквивалентна и = и - v. В листинге 3.5 приведен код операторов += и -= для классов векторов. 8 файле PMMathLibV3. h в папке Source\Chapter03 на компакт-диске содержатся версии этих классов с операторами += и -=. Листинг 3.5. Операторы +- и -= 1 // Вставить в класс vector_2d 2 inline vector_2d vector_2d::operator +=(vector_2d SrightOperand) 3 { 4 x+=rightOperand.x; 5 y+=rightOperand.у; 6 return(*this); 7 } 8 9 inline vector_2d vector_2d::operator -=(vector_2d SrightOperand) 10 { 11 x-=rightOperand.x; 12 y-=rightOperand.у; 13 return(*this); 14 } 15 16 // Вставить в класс vector_3d 17 inline vector_3d vector_3d::operator +=(vector_3d SrightOperand) 18 { 19 20 21 22 23 > 24 25 inl 26 { 27 28 29 30 31 } x+=rightOperand.x; y+=rightOperand.у; z+=rightOperand.z; return(*this) ; ine vector 3d vector 3d::operator x-=rightOperand.x; y-=rightOperand.у; z-=rightOperand.z; return(*this); -=(vector_3d SrightOperand)
68 Глава 3 Поскольку эти операторы изменяют значение переменных, стоящих слева от них в выражениях, они не могут возвращать безымянную временную переменную, как операторы + и -. Умножение и деление вектора на скаляр Умножение векторов можно выполнять разными способами. Первый способ - умножение вектора на скаляр (число). При таком умножении изменяется длина вектора, но не его направление (см. рис. 3.14). При этом число может называться коэффициентом масштабирования. Произведение скалярного числа (а) и вектора (v) записывается в виде av. Операция умножения вектора на скаляр коммутативна, поэтому av = va. В двумерных и трехмерных декартовых системах координат векторы записываются с помощью компонентов (х, у) или (х, у, z), соответственно. Скалярное умножение векторов в компонентной форме сводится к умножению каждого компонента на скаляр. Если компоненты вектора v - (vx, v , vz), то компоненты вектора av - (avx, av , avz). Кроме того, можно разделить вектор на скаляр. Это то же самое, что умножить вектор на обратную величину скаляра, то есть деление вектора на 2 уменьшает его длину в 2 раза. Рис. 3.14. Масштабирование вектора В листинге 3.6 приведен код операторов умножения и деления для обоих классов векторов. Листинг 3.6. Операторы умножения и деления для обоих классов векторов 1 inline vector_2d vector_2d::operator *(scalar rightOperand) 2 { 3 return(vector_2d(x*rightOperand,y*rightOperand)); 4 } 5 6 inline vector_2d operator *(scalar leftOperand, 7 vector_2d &rightOperand) 8 { 9 return(vector_2d(leftOperand*rightOperand.x, 10 leftOperand*rightOperand.y)); 11 ) 12 13 inline vector_2d vector_2d::operator *=(scalar rightOperand) 14 { 15 x*=rightOperand; 16 y*=rightOperand; 17 return(*this); 18 }
Математические инструменты 69 19 20 inline vector_2d vector_2d::operator /(scalar rightOperand) 21 { 22 return(vector_2d(x/rightOperand,y/rightOperand)); 23 } 24 25 inline vector_2d vector_2d::operator /=(scalar rightOperand) 26 { 27 x/=rightOperand; 28 y/=rightOperand; 29 return(*this); 30 } 31 32 inline vector_3d vector_3d: .-operator * (scalar rightOperand) 33 { 34 return(vector__3d(x*rightOperand,y*rightOperand,z*rightOperand)); 35 } 36 37 inline vector_3d operator *(scalar leftOperand, 38 vector_3d SrightOperand) 39 { 40 return(vector_3d(leftOperand*rightOperand.x, 41 leftOperand*rightOperand.у, 42 leftOperand*rightOperand.z)); 43 } 44 45 inline vector_3d vector_3d::operator *=(scalar rightOperand) 46 { 47 x*=rightOperand; 48 y*=rightOperand; 49 z*=rightOperand; 50 return(*this); 51 ) 52 53 inline vector_3d vector_3d::operator /(scalar rightOperand) 54 { 55 return(vector_3d(x/rightOperand,y/rightOperand,z/rightOperand)); 56 ) 57 58 inline vector_3d vector_3d::operator /=(scalar rightOperand) 59 { 60 x/=rightOperand; 61 y/=rightOperand; 62 z/=rightOperand; 63 return(*this); 64} Поскольку скалярное умножение коммутативно, в каждом классе есть по две версии оператора умножения. Первая версия использует вектор в качестве левого операнда и скаляр - в качестве правого. Вторая версия использует в качестве левого операнда скаляр, а в качестве правого —
70 Глава 3 вектор. В бинарных операциях языка C++ функция-оператор всегда вызывается левым операндом. Соответственно, если в выражении вектор будет левым операндом, умножение пройдет без проблем. Однако если левый операнд - скаляр, то он не сможет вызвать функции-операторы классов векторов. Поэтому операции умножения, использующие скаляр в качестве левого операнда, нельзя реализовать как методы классов. Они должны быть дружественными функциями для этого класса. Прототипы двух этих функций объявлены в классах vector_2d и vector_3d так, как показано ниже: // Из класса vector_2d friend vector_2d operator *(scalar leftOperand, vector_2d SrightOperand); //Из класса vector_3d friend vector_3d operator *(scalar leftOperand, vector_3d SrightOperand); В строках 6-11 листинга 3.6 приведен код дружественной функции- оператора умножения для класса vector_2d. Заметьте, что ключевое слово friend не нужно ставить в первую строку функции. Оно должно присутствовать только в прототипе функции в определении класса. В листинге 3.6 также приведен код операторов *= для каждого класса векторов. Кроме того, в листингах приведен код операторов / и /=. Скалярное произведение векторов Кроме умножения вектора на скаляр, вектор можно умножить и на другой вектор. На самом деле умножать вектор на вектор можно несколькими способами. Один из них называется скалярным произведением (scalar product). Это произведение двух векторов не следует путать с результатом умножения вектора на скаляр. Замечание Результат умножения не может зависеть от того, какую вы выбрали систему координат. Это основное ограничение для умножения. Скалярное произведение следует этому правилу; независимо от используемой системы координат скалярное произведение одних и тех же векторов будет одним и тем же числом. Иногда скалярное произведение называют внутренним произведением (inner product). Вычисление скалярного произведения векторов — это не то же самое, что вычисление результата умножения вектора на скаляр. Вычисляя скалярное произведение, мы перемножаем два вектора, чтобы в результате получить скаляр. Скалярное произведение записывается так: а • b
Математические инструменты 71 В компонентной форме скалярное произведение выглядит так: а • b = axbx + ayby для двумерных векторов а • b = axbx + ayby + azbz для трехмерных векторов а • b = axbx + ayby + azbz+ a^^, для четырехмерных векторов Если компоненты векторов неизвестны, то скалярное произведение этих векторов можно вычислить. Если известны магнитуды этих векторов и угол между ними (в), как показано на рисунке 3.15, то скалярное произведение можно найти по формуле а • b = ab cosF>) v Рис. 3.15. Скалярное произведение векторов Заметьте, что в этой формуле буквы а и b (не выделенные жирным шрифтом) — это магнитуды векторов а и b соответственно. Скалярное произведение векторов коммутативно, поэтому а • b = b • а. В этом можно убедиться, используя любое из приведенных выше уравнений. Скалярное произведение векторов также обладает дистрибутивностью - это значит, что a*(b + c) = a,b + a»c. В листинге 3.7 приведен код методов классов vector_2d и vec- tor_3d для вычисления скалярного произведения векторов. Листинг 3.7. Методы, вычисляющие скалярное произведение векторов 1 //Из класса vector_2d 2 inline scalar vector_2d::Dot(const vector_2d Svl) 3 { 4 return(x*vl.x + y*vl.y); 5 > 6 7 //Из класса vector_3d 8 inline scalar vector_3d::Dot(const vector_3d Svl) 9 { 10 return(x*vl.x + y*vl.y + z*vl.z); 11} Как уже говорилось ранее, магнитуда вектора также называется его нормой. Скалярное произведение дает возможность вычислить норму вектора. Если скалярно перемножить вектор сам с собой, в результате мы получим квадрат его нормы, как видно из следующих формул: а • а = аа cos@) = a2
72 Глава 3 а= а Разумеется, если скалярно умножать вектор сам на себя, то угол будет равен 0. Поскольку косинус нуля равен 1, этот член можно игнорировать. В результате мы получаем уравнение, похожее на то, которое получали, обсуждая треугольники. Это теорема Пифагора. В математике множество таких совпадений. Выражение для вычисления нормы было дано для двумерных векторов, но оно работает и для трехмерных векторов. В этом случае оно примет вид *=fix ax+ayay+azaz Следующий шаг - воплотить эти формулы в коде программ. Однако прежде чем мы этим займемся, сделаем важное замечание: если вы можете обойтись квадратом нормы, сделайте это. Квадратные корни вычисляются очень медленно. Именно поэтому в библиотеке присутствуют два метода для вычисления норм: один для вычисления собственно нормы, а второй — для вычисления квадрата нормы. Код этих методов приведен в листинге 3.8. Листинг 3.8. Вычисление норм векторов 1 //Из класса vector_2d 2 inline scalar vector_2d::Norm(void) 3 { 4 return(sqrt(x*x + y*y)); 5 } 6 7 inline scalar vector_2d::NormSquared(void) 8 { 9 return(x*x + y*y); 10 } 11 12 // Иа класса vector_3d 13 inline scalar vector__3d: : Norm (void) 14 { 15 return(sqrt(x*x + y*y + z*z)); 16 } 17 18 inline scalar vector_3d::NormSqua£ed(void) 19 { 20 return(x*x + y*y + z*z); 21 }
Математические инструменты 73 Прежде чем завершить обсуждение скалярных произведений, замечу, что с их помощью можно определить, перпендикулярны ли друг к другу (или ортогональны) два вектора, то есть равен ли 90° угол между ними. Скалярное произведение ортогональных векторов равно 0. Поэтому, если функции из листинга 3.8 возвращают 0, вы будете знать, что векторы, использованные в качестве аргументов, ортогональны друг к другу. Векторное произведение векторов При скалярном произведении двух векторов результат является скаляром. При векторном произведении двух векторов результатом будет вектор. Векторное произведение записывается в виде: а х b В отличие от скалярного произведения и других операций с векторами, рассматриваемых в этой главе, векторное произведение существует только в трехмерных координатах. Для любых двух векторов можно найти плоскость, в которой они лежат, как показано на рисунке 3.16. Вектор, получаемый при векторном произведении двух векторов, будет перпендикулярен плоскости, в которой лежат эти два вектора. Получаемый вектор называется нормальным вектором (normal vector). Именно для его получения и предназначено векторное произведение векторов. v Рис. 3.16. Два вектора и плоскость, в которой они лежат Предположим, что два вектора определяют горизонтальную плоскость. В какую сторону - вверх или вниз - будет направлен нормальный вектор? Чтобы ответить на этот вопрос, воспользуемся правилом правой руки. Это правило сводится к следующему: чтобы определить направление, в котором указывает нормальный вектор, вытяните правую руку в направлении первого исходного вектора и направьте пальцы в ту сторону, в которой относительно первого исходного вектора расположен второй, как показано на рисунке 3.17. Отогните большой палец. Он будет указывать в направлении, в котором должен указывать нормальный вектор. Для вычисления векторного произведения векторов, которые мы обозначим и и v, служит следующая формула: и х v = (uyvz - uzvy)i + (uzvx - uxvz)j + (uxvy - uyvx)k В этой формуле i, j и к - это единичные векторы, с которыми вы скоро познакомитесь. А пока мы приведем формулу к виду, который немного легче использовать для написания кода на C++:
74 Глава 3 A u Xv Рис. 3.17. Правило правой руки для векторного произведения векторов Г = U X V гх ГУ rz = uyvz - = uzvx " = uxvy - -uzvy "UXVZ -uyvx Эти уравнения дают нам компоненты вектора г, получающегося в результате векторного перемножения векторов и и v. По этим формулам работает метод вычисления векторного произведения, код которого приведен в листинге 3.9. Листинг 3.9. Метод вычисления векторного произведения векторов 1 2 з 4 5 6 7 8 9 inline vector_3d vector_3d::Cross(const vector_3d SrightOperand) { return( vector_3d( y*rightOperand.z - z*rightOperand.x - z*rightOperand.у, x*rightOperand.z, x*rightOperand.y - y*rightOperand.x)); Единичные векторы Есть еще один способ компонентного представления векторов, который иногда бывает очень полезным. Можно определить двумерную декартову систему координат с помощью двух маленьких векторов длиной 1, один из которых направлен вдоль оси х, а другой - вдоль оси у, как на рисунке 3.18. Обозначим эти векторы х и у (читается как «х с шапочкой» и «у с
Математические инструменты 75 шапочкой»). Шапочка (символ Л) означает, что это единичные векторы (unit vectors), нормы которых равны 1. В их координатной системе компоненты вектора х есть A, 0), а вектора у - @, 1). I 1 Ь ■>+ Рис. 3.18. Единичные векторы в двумерных координатах Иногда единичные векторы обозначаются не х и у, a i и j. В любом случае это векторы с нормой 1, направленные вдоль осей х и у. Любой вектор в декартовой системе координат можно представить в виде суммы произведений единичных векторов и скаляров. Другими словами, для любого вектора v = ах + by для некоторых а и Ь. Здесь а и b есть координаты вектора в системе координат, определенной единичными векторами х и у. Поэтому если а = 2 и b = 3, то координаты вектора равны B, 3), и вектор можно записать в виде2х + 3у или 2i + 3j. Единичные векторы для третьего и четвертого измерений обычно обозначаются Z и W или кит. Их компоненты равны соответственно @, 0, 1) и @, 0, 0, 1). Замечание Обозначение ( W или m ) нечасто встречается в книгах по математике. Чаще оно применяется в программировании ЗО-графики. Иногда требуется найти единичный вектор, направление которого совпадает с направлением другого вектора. Операция нахождения такого вектора называется нормализацией. Она довольно проста. Если разделить вектор на его длину, вы получите единичный вектор с тем же направлением, что и исходный. Единичный вектор обозначается так же, как и исходный, но с шапочкой: V V
76 Глава 3 В листинге 3.10 приведен код методов нормализации векторов для классов vector_2d и vector_3d. Листинг 3.10. Методы нормализации векторов 1 inline vector_2d vector_2d::Normalize(scalar tolerance) 2 { 3 vector_2d result; 4 5 scalar length = Norm(); 6 if (length>=tolerance) 7 { 8 result.x = x/length; 9 result.у = у/length; 10 } 11 return(result); 12 } 13 14 inline vector_3d vector_3d::Normalize(scalar tolerance) 15 { 16 vector_3d result; 17 18 scalar length = Norm(); 19 if (length>=tolerance) 20 { 21 result.x = x/length; 22 result.у = y/length; 23 result.z = z/length; 24 } 25 return(result); 26 ) Вероятно, вы заметили, что эти функции проверяют длину вектора. Если она меньше минимальной, указанной в переменной tolerance, то функции не выполняют деление, а просто возвращают значение 0 (деление на 0, скорее всего, приведет к аварийному завершению выполнения программы). Проецирование Скалярное умножение вектора на единичный вектор называется проецированием или определением проекции вектора на единичный вектор. Проецирование выделяет компонент вектора, направленный параллельно единичному вектору, как показано на рисунке 3.19. Например, перемножая вектор v с единичным вектором х , мы получим компонент х вектора v, то есть, vx, как показано ниже: v#x=(vxi+vyy+vzz)»i=vxx,x+vyy«x+vzz'i=vx
Математические инструменты 77 Рис. 3.19. Проецирование вектора v на единичный вектор п ПРИМЕР: ОТСКАКИВАНИЕ ОТ СТЕНЫ Проецирование векторов часто используется для моделирования столкновений объектов. Например, можно смоделировать столкновение мяча со стеной. Эта задача называется отражением вектора (vector reflection). Одна из первых компьютерных игр Pong была имитацией настольного тенниса, основанной на отражении двумерных векторов. Чтобы можно было выполнить моделирование, нам понадобятся два вектора, один из которых описывает движение мяча, а второй - расположение стены. Определим единичный вектор, перпендикулярный стене, как на рисунке 3.20. Обозначим его П. Вектор v будет описывать перемещение мяча (его скорость и направление движения). Еще один вектор v' (читается как «v-штрих») будет описывать движение мяча после отскока от стены. Проблема заключается в следующем: зная начальную скорость мяча и располагая единичным вектором, перпендикулярным к стене, нам нужно найти скорость мяча после столкновения. Заметьте, что на чертеже нет координатных осей; эту задачу можно решить без использования систем координат, поскольку она носит общий характер. Если что-то упруго отскакивает от стены, то компонент скорости, перпендикулярный к стене, изменяет свою величину на противоположную по знаку, а компонент, параллельный стене, остается неизменным.
78 Глава 3 Замечание Когда говорят, что столкновение является упругим, то подразумевается, что стена твердая, а сталкивающийся с ней предмет представляет собой нечто вроде теннисного мяча, который отскочит от стены после столкновения. Если со стеной столкнется комок грязи или снежок, то отскока не будет, и такое столкновение называется неупругим. Мы поговорим о столкновениях подробнее, когда доберемся до посвященной им главы. Поскольку в нашей задаче моделируется упругое столкновение, нам нужно только найти компонент вектора v', перпендикулярный стене, то есть компонент, направленный параллельно вектору П . Компонент вектора v", параллельный стене, можно просто скопировать из такого же компонента вектора v. Чтобы найти компонент вектора v в направлении вектора й , нужно найти скалярное произведение вектора v и вектора П и нормализовать его по формуле (х* n) n • Будет полезно присвоить обозначение компоненту вектора v, направленному параллельно стене, хотя нам никогда не понадобится его вычислять. Обозначим его р. Вся информация, которая у нас есть в данный момент, изображена на чертеже (см. рис. 3.21). Рис. 3.21. Поместим на чертеж всю наличную информацию Теперь вектор v можно записать в виде суммы компонентов: v = р + (v • п )п Важно заметить, что в этой формуле р одинаков и для v, и для v'. Компонент меняет знак на противоположный. Поэтому v' = р - (v • п )п А теперь найдем р из первого уравнения и избавимся от присутствия р во втором уравнении:
Математические инструменты 79 v'= v - (v • n)n-(v* n)n = v - 2(v • n )n Поскольку мы не использовали какую-то конкретную систему координат, полученный нами результат верен для любой системы координат. Здорово, правда? ОТСКАКИВАНИЕ ОТ СТЕНЫ: ПРАКТИЧЕСКАЯ ПРОВЕРКА Дабы убедиться в верности полученного результата, проверим его. Для этого подставим в формулу реальные числа и проведем вычисления, чтобы получить результат. Предположим, что игрок стреляет, и пули рикошетят от стены. Компоненты вектора скорости пуль равны (-3 м/с, 4 м/с). Единичный вектор, перпендикулярный к стене, обозначен X. Скорость пули есть магнитуда вектора ее скорости. Поэтому первый наш шаг - нахождение нормы v вектора скорости пули v. v = V v • v = V (-3 м/с, 4 м/с) • (-3 м/с, 4 м/с)" = V (-3 м/с)(-3 м/с) + D м/с)D м/с)" = V 9 м2/с2 + 16 м2/^2 = V 25 м2/с2 = 5 м/с Конечно, 5 метров в секунду - очень медленно для пули. Но для нашей задачи это не слишком существенно. Перед тем как найти вектор скорости пули после ее столкновения со стеной, найдем проекцию вектора v в направлении вектора х- Затем можно подставить v и х в выведенные выше формулы: v • X = ( (-3 м/с) X + D м/с) У ) • X = -3 м/с v' = v - 2(v • х ) х = (-3 м/с) i + D м/с) у - 2(-3 м/с) х = C м/с) х + D м/с) у Компонент у (параллельный стене) не изменился, а знак компонента х изменился на противоположный.
80 Глава 3 Векторы в Direct 3D Поскольку векторы очень важны в ЗБ-программировании, то вспомогательная библиотека D3DX для Direct3D содержит несколько структур и функций для работы с двумерными, трехмерными и четырехмерными векторами. Хотя в библиотеке физического моделирования есть все, что нам понадобится для работы с векторами, учтите, что с ними можно работать и с помощью Direct3D. Мы будем использовать функции библиотеки Direct3D, работая с ЗБ-графикой, но для физических расчетов мы будем пользоваться только нашей библиотекой физического моделирования. Это позволит нам сделать большую часть кода легко переносимой между разными платформами. Если вы захотите применять для написания игр вместо DirectX что-то другое, вы сможете использовать большую часть кода из этой книги. Структуры для векторов называются D3DXVECTOR2, D3DXVECTOR3 и D3DXVECTOR4 для двумерных, трехмерных и четырехмерных векторов соответственно. В этих структурах под каждый компонент вектора отведена одна переменная типа float. Чтобы эти структуры можно было использовать в библиотеке моделирования, нам нужна возможность преобразования классов векторов из библиотеки моделирования в структуры векторов из DirectX и обратно. В библиотеке моделирования уже есть функции преобразования из структур D3DXVECTOR2 и D3DXVECTOR3 в объекты классов vector_2d и vector_3d. Это конструкторы классов. Для преобразования объектов классов vector_2d и vector_3d в структуры D3DXVECTOR2 и D3DXVECTOR3 нам понадобятся операторы приведения типов в классах. Код этих операторов приведен в листинге 3.11. Листинг 3.11. Операторы приведения типов 1 vector_2d::operator D3DXVECTOR2() 2 { 3 return (D3DXVECTOR2(x,y)); 4 } 5 6 vector_3d::operator D3DXVECTOR3() 7 { 8 return (D3DXVECTOR3(x,у,z)); 9 } Чтобы выполнить преобразование, эти два оператора создают безымянные временные переменные типов D3DXVECTOR2 и D3DXVECTOR3, записывают в них компоненты векторов и используют эти переменные в качестве возвращаемых значений.
Математические инструменты 81 Матрицы Матрицы - это просто массивы чисел, которые можно складывать и перемножать по определенным правилам. Вы, вероятно, знаете, что такое массивы в программировании, даже если вы не встречались с ними где-то еще. В физике и математике матрицы обозначаются заглавными буквами, например, М. Элементы матрицы обычно обозначаются строчными буквами с двумя нижними индексами, поэтому матрица может выглядеть так: м = mU т12 Ш13 т14 т21 т22 т23 т24 т31 т32 т33 т34 т41 т42 т43 т44 В математике и физике нумерация столбцов и строк в матрицах обычно начинается с единицы. Однако в компьютерах для хранения двумерных матриц обычно используются двумерные массивы, а индексы в массивах в языке C++ начинаются с О. Поэтому во многих книгах на компьютерные темы нумерация столбцов и строк в матрицах тоже начинается с 0. Именно так нумеруются столбцы и строки в матрицах и в этой книге. Матрица М называется матрицей 4x4, поскольку в ней четыре столбца и четыре строки. В программировании графики на компьютере чаще всего используются квадратные матрицы, в которых количество строк равно количеству столбцов. Это утверждение верно и для тех областей физики, с которыми мы будем иметь дело. Почти все матрицы, которые нам встретятся, будут квадратными. В библиотеке моделирования реализованы классы matrix2x2 и matrix3x3 для работы с матрицами 2x2 и 3x3. С многих точек зрения матрицы проще векторов. Они не привязаны к каким-то координатам, как векторы. Они больше похожи на компоненты векторов в определенных системах координат. Собственно говоря, компоненты n-мерных векторов ведут себя очень похоже на матрицы размера 1хп. Учтя эту схожесть матриц и компонентов векторов, вероятно, вы не удивитесь, узнав, что над матрицами можно выполнять почти такие же операции, как и над векторами: сложение, умножение на скаляр и умножение на другую матрицу. Однако будьте внимательными. У матриц есть свои особенности. Замечание Не так уж сложно создать класс, который сможет работать с матрицами любого размера. Если вы хотите создать такой класс, прочитайте, например, книгу «Numerical Recipes for C++» (издательство William Press and Company). Однако мы сейчас изучаем специфическую область программирования - программированию игр. Нам понадобятся только матрицы определенных размеров. Универсальность стоит принести в жертву скорости работы. Реализуя только те матрицы, которые нам понадобятся, можно упростить код и немного ускорить его выполнение.
82 Глава 3 В листинге 3.12 приведены определения классов матриц. Листинг 3.12. Классы matrix2x2 и matrix3x3 1 class matrix2x2 2 { 3 private: 4 scalar elements[2][2]; 5 6 public: 7 matrix2x2(void); 8 matrix2x2(scalar initializationArray[2][2]) ; 9 matrix2x2(scalar mOO,scalar mOl,scalar mlO,scalar mil); 10 11 void Element(int row,int col,scalar elementValue); 12 scalar Element(int row,int col); 13 14 matrix2x2 Soperator =(scalar initializationArray[2][2]); 15 }; 16 17 class matrix3x3 18 { 19 private: 20 scalar elements[3][3]; 21 22 public: 23 matrix3x3(void); 24 matrix3x3(scalar initializationArray[3][3]); 25 matrix3x3(scalar mOO,scalar mOl,scalar m02, 26 scalar mlO,scalar mil,scalar ml2, 27 scalar m20,scalar m21,scalar m22); 28 29 void Element(int row,int col,scalar elementValue); 30 scalar Element(int row,int col); 31 32 matrix3x3 Soperator =(scalar initializationArray[3][3]); 33 }; В этих классах, как и в ранних версиях классов векторов, есть только элементы данных и методы записи и чтения значений этих элементов. В нескольких последующих разделах мы добавим в них возможности выполнения различных операций над матрицами. Однако их код обычно весьма прост и в книге по большей части не приводится. На компакт-диске, поставляющемся с книгой, содержится полная версия библиотеки моделирования, в которой реализованы все методы, выполняющие операции над матрицами. Вы найдете эту библиотеку в папке Source\Chap- ter03\Vectors and Matrices, в файле PMMathLibVlO.h.
Математические инструменты 83 Единичная матрица Одна из простейших операций, которые можно выполнить над матрицей, - инициализация ее единичной матрицей. Единичная матрица (unit matrix) - это квадратная матрица, все элементы которой равны 0, кроме элементов, расположенных на диагонали, идущей от левого верхнего угла в правый нижний. Это описание легче понять, если рассмотреть пример ниже. Единичная матрица размера 2x2 выглядит следующим образом: 1= 1 О О 1 Единичная матрица 3x3 имеет вид 10 0 0 1 0 0 0 1 Как видите, в единичной матрице все единицы расположены на одной диагонали (эта диагональ называется главной), а остальные элементы равны 0. Код функций Identity (), формирующих единичные матрицы, есть в файле с исходным кодом библиотеки PMMathLibVIO .h на компакт-диске. Замечание Единичную матрицу иногда называют матрицей идентичности (identity matrix). Умножение матрицы на единичную матрицу дает ту же самую матрицу, то есть фактически ничего не делает: AI = IA = A Сложение и вычитание матриц Сложение матриц выполняется очень просто. Складывать друг с другом можно только матрицы одинакового размера. При этом складываются соответствующие элементы матриц. Пусть у нас есть матрицы А и В, показанные ниже: а00 а01 40 *02 а11 а12 а20 а21 а22
84 Глава 3 В= 7оо '10 Ь01 Ь02 Jll .^20 ^21 J12 Э22 Тогда результатом сложения этих матриц будет такая матрица: А + В = аоо+Ь(>о аю+ью а20+ °20 а01+Ь01 ап+Ьп а21+Ь21 а02+Ь02 а12+Ь12 а22+Ь22 Вычитание одной матрицы из другой выполняется аналогично: А-В = а00"°00 а01"1 а02" "Э02 а10~Ь10 а1ГЬ11 l_a20-b20 a21-b21 а12"Ь12 а22-Ь22 В классах matrix2x2 и matrix3x3 реализованы операторы +, +=, - и -=. В классе matrix2x2 элементы матрицы прямо перечисляются в коде операторов, поскольку этих элементов всего четыре. Однако в классе matrix3x3 для перебора элементов и выполнения сложения используется пара вложенных циклов. Хотя это несколько менее эффективно, код получается более понятным. Умножение и деление матрицы на скаляр Чтобы умножить матрицу на скаляр, нужно просто умножить на этот скаляр каждый элемент матрицы. Пусть у нас есть матрица А и скаляр w. Тогда, если то А = wA = а оо 110 wa 00 wa ю l01 41. wa 01 wa и В каждом из классов — matrix2x2 и matrix3x3 - есть по два оператора, выполняющих умножение матрицы на скаляр. Один из операторов выполняет умножение, если левым операндом является матрица. Второй
Математические инструменты 85 оператор реализован как дружественная функция, выполняющая умножение, если левым операндом является скаляр. Эти операторы можно использовать так: matrix2x2 ml, ю2A, 2, 3, 4); scalar s = 5; ml = m2 * s; Или то же самое можно записать в виде matrix2x2 ml, m2(l, 2, 3, 4); scalar s = 5; ml = s * m2; И тот, и другой вариант будут работать. В классах также есть операторы *=, позволяющие умножить матрицу на скаляр и сохранить результат в той же матрице. Деление матрицы на скаляр выполняется примерно так же, как умножение. Чтобы разделить матрицу на скаляр, нужно разделить на этот скаляр каждый элемент матрицы, как показано ниже: A/w = а00 а01 _аю ап_ a00/w a01/w" a10/w an /w В отличие от умножения, деление не коммутативно, поэтому в классах matrix2x2 и matrix3x3 есть только по одному оператору /. Однако в классах есть операторы /=. Перемножение матриц Кроме умножения матрицы на скаляр, одну матрицу можно умножить на другую, получив в результате новую матрицу. Однако есть ограничения на размеры перемножаемых матриц. Две матрицы можно перемножить только в том случае, если количество столбцов в первой матрице равно количеству строк во второй. То есть, можно перемножить матрицы рхп и nxq, но нельзя перемножить матрицы рхп и qXn. Результатом перемножения матриц рхп и nxq будет матрица размера pxq (с р строками и q столбцами). Замечание Иногда перемножение матриц называют в программировании конкатенацией матриц.
86 Глава 3 Вот пара примеров матриц, которые можно перемножать. Перемножение матриц размерами 2x3 и 3x7 даст в результате матрицу 2x7. Перемножение матриц размерами 2x3 и 3x5 даст в результате матрицу 2x5. Перемножить матрицы размерами 2x3 и 5x3 нельзя. Сам метод перемножения на первый взгляд может показаться запутанным, но со временем вы привыкнете к нему. Перемножение матриц можно рассматривать по-разному. Я предложу пару подходов, и вы сможете решить, какой вам больше нравится. Первый подход - воспринимать перемножение матриц как последовательности скалярных произведений векторов. Взгляните на строку первой матрицы. Она выглядит как набор компонентов вектора, правда? А теперь посмотрите на первый столбец второй матрицы. Он тоже выглядит как набор компонентов вектора. А теперь можно «перемножить» эти два вектора, чтобы получить скаляр. Этот скаляр будет значением на пересечении первого столбца и первой строки результирующей матрицы. Вот пример того, как перемножаются матрицы. Предположим, что у нас есть матрица 3x3: 2 4 3 4 5 2 4 4 1 Умножим ее на матрицу В размером 3x2: В= 12" 23 43 Результатом перемножения будет матрица 3x2. Чтобы получить ее элемент @, 0), нужно перемножить первую строку матрицы А и первый столбец матрицы В: B 4 3)'A 2 4) = BI + DJ + CL = 2 + 8 + 12 = 22 Элемент @, 0) равен 22. Результирующая матрица на данный момент выглядит так: 22
Математические инструменты 87 Элемент (О, 1) - первая строка, второй столбец - получается в результате перемножения первой строки матрицы А и второго столбца матрицы В: B 4 3)»B 3 3) = BJ + DK + CK = 4 + 12 + 9 = 25 Теперь результирующая матрица выглядит как 2 25" Продолжим. Элемент A, 0) - вторая строка, первый столбец - получается в результате перемножения второй строки матрицы А и первого столбца матрицы В: D 5 2)»A 2 4) = DI + EJ + BL = 4 + 10 + 8 = 22 Теперь результирующая матрица будет выглядеть так: 2 25" 22 Если мы продолжим перемножать элементы, мы получим следующий результат:  4 3" 4 5 2 4 4 1 2" 23 43 = 225" 2225 16 23 Этот метод перемножения матриц просто запомнить, и он иллюстрирует математическую связь между матрицами и компонентами векторов. Второй метод, который я хочу продемонстрировать, - это просто формула. Основное преимущество этого метода в том, что его можно объяснить одной строкой. Если А - матрица размером ixn, а В — матрица размером nxj, то верно следующее: АВ=С в том и только в том случае, если п-1 Cij = 5>ikbkj= ai0b0j+ ailV-+ ai(n-Db(n-DJ k=0 Знак 2 — это заглавная греческая буква сигма, обозначающая сумму.
88 Глава 3 Если в формуле есть обозначение п 1 к=1 оно означает сумму того, что находится справа от этого обозначения, причем суммируются п элементов, в первом из которых к=1, во втором - к=2 и так далее до k=n. Например, ^2=2+2+2+2=8 k=i 4 £k = l+2 + 3 + 4=10 k=l Используя формулу для матриц из предыдущего примера, мы получим: АВ = 2 4 4 4 3" 5 2 4 1_ 2" 23 43 3 I. к=0 соо =£ aokbko = aooboo + aoibio+ а02Ь20 = 2'1 + 4-2 + 3-4 = 2 + 8 + 12 = 22 Поскольку перемножение двух матриц - операция более замысловатая, чем рассмотренные ранее операции с матрицами, рассмотрим код методов перемножения матриц. Этот код приведен в листинге 3.13. Листинг 3.13. Умножение матрицы на матрицу 1 matrix2x2 matrix2x2::operator * (const 2 matrix2x2 SrightOperand) 3 { 4 return( 5 matrix2x2( 6 // Значение элемента 0,0 7 elements[0][0]*rightOperand.elements[0][0] + 8 elements[0][1]*rightOperand.elements[1][0], 9 // Значение элемента 0,1 10 elements[0][0]*rightOperand.elements[0][1] + 11 elements[0][1]*rightOperand.elements[1][1], 12 // Значение элемента 1,О 13 elements[1][0]*rightOperand.elements[0][0] +
Математические инструменты 89 14 15 16 17 18 } 19 elements[1][1]*rightOperand.elements[1][0], // Значение элемента 1,1 elements[1][0]*rightpperand.elements[0][1] + elements[1][1]*rightOperand.elements[1][1])); 20 matrix3x3 matrix3x3::operator *(const matrix3x3 SrightOperand) 21 { 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 } matrix3x3 answer; for (int i=0;i<3;i++) { for (int j=0;j<3;j++) { answer.elements[i][j] = elements[i][0]*rightOperand.elements[0][j] + elements[i][1]*rightOperand.elements[1][j] + elements[i][2]*rightOperand.elements[2][j]; } ) return(answer); В матрице 2x2 нужно вычислить всего четыре элемента, и это в явном виде делается в строках 7-17 листинга 3.13. Явное вычисление каждого элемента матрицы 3x3 потребует слишком объемного кода, поэтому они вычисляются с помощью двух циклов (строки 24-33 в листинге 3.13). У перемножения матриц есть несколько специфических свойств. Вероятно, самое важное из них то, что перемножение матиц не коммутативно, за исключением некоторых особых случаев, то есть АВ Ф ВА Однако перемножение матриц ассоциативно и дистрибутивно, то есть (АВ)С = А(ВС) А(В + С) = АВ + АС Транспонирование матриц Транспонировать матрицу - значит сделать ее столбцы строками и наоборот. Транспонированная матрица А обозначается Ат. Например, если матрица А имеет вид
90 Глава 3 а00 а01 а02 а10 ап а12 а20 а21 а22 а30а31 а32. то Ат будет иметь вид Ат = аООа10а20аЗО а01 ап а21 а31 а02а12а22а32 Определители Определители или детерминанты (determinants) - это скалярные свойства матриц. Что такое определитель матрицы, довольно сложно объяснить коротко, но вот простой пример. Определитель матрицы 2x2 равен разности произведений элементов по диагоналям, например: А= а оо Чо а 01 1п det[A]-a00an-a01a10 Определитель матрицы 3x3 вычислить сложнее. Его можно записать в виде суммы определителей 2x2: А = 100 40 120 а01 а02 а11 а12 а21 а22. det[A] = a00det ап ai2 a21 a22 aQ1det aio ai2 a20 a22 + a02det aio an La20 a21. Подставив в последнее выражение формулу для определителя 2 X 2 и сократив элементы с противоположными знаками, получим:
Математические инструменты 91 det[A] - а00« ап* а22- а00* а21« а]2+ а10« а21« а02- а10» а0,- а22+ + а20* aoi* а12" а20* аи* а02 Это выражение легко преобразовать в операторы языка C++, как показано в листинге 3.14. Листинг 3.14. Методы Determinant() классов matrix2x2 и matrix3x3 1 scalar matrix2x2::Determinant() 2 { 3 return(elements[0][0]*elements[1][1] - 4 elements[1][0]*elements[0][1]); 5 } 6 7 scalar matrix3x3::Determinant() 8 { 9 return (elements[0][0]*elements[1][l]*elements[2][2] - 10 elements[0][0]*elements[2][1]*elements[l][2] + 11 elements[1][0]*elements[2][1]*elernents[0][2] - 12 elements[1][0]*elements[0][1]*elements[2][2] + 13 elements[2][0]*elements[0][1]*elements[1][2] - 14 elements[2][0]*elements[1][1]*elements[0][2]); 15 } Как видите, эти функции просто вычисляют возвращаемые значения по приведенным выше формулам. Обращение матриц Нам часто будут нужны матрицы, обратные тем, что у нас есть. Обратная матрица обозначается символом исходной и показателем степени —1, как если бы мы возводили матрицу в степень —1, например, обратную матрицу матрицы А обозначают А-1. Обратная матрица определяется следующим образом: АА-!= I Вспомните, что I - это единичная матрица. Из этой формулы следует, что умножение матрицы на обратную ей матрицу даст в результате единичную матрицу. Этот процесс аналогичен умножению обычного числа на обратное ему число. Если умножить 2 на 1/2, мы получим 1. Если у нас, к примеру, есть произведение матриц А и В, то мы можем получить из него матрицу А, умножив матрицу АВ на В-1: (АВ)В-1 = А(ВВ-Х) = А
92 Глава 3 Обращение обратной матрицы даст нам исходную матрицу: (А) = А Не у всех матриц есть обратные матрицы. Можно найти обратную матрицу только для квадратной матрицы (у которой количество строк равно количеству столбцов), определитель которой не равен 0. В физике и программировании нам в основном будут встречаться квадратные матрицы, определитель которых не равен 0, и обратные им матрицы всегда можно вычислить. Обратную матрицу можно найти по следующей формуле: -1 det[A] Предупреждение В практических задачах из физики и математики обращение матриц обычно выполняется не через определитель - этот способ требует слишком большого количества вычислений для матриц более-менее заметного размера. Здесь он рассматривается из-за простоты его понимания. Я очень рекомендую вам изучить способ нахождения обратных матриц методом Гауссова исключения. Описание этого способа легко найти в Интернете, но будьте готовы к тому, что придется разобраться в математике несколько более сложной, чем использованная в этой главе. Матрица С называется присоединенной матрицей (cofactor matrix). Нам нет особой нужды разбираться в способах нахождения присоединенных матриц для любых исходных. Мы рассмотрим только способ нахождения присоединенных матриц, который устроит нас. Если вас интересуют общие методы или вам любопытно, как получены приведенные выше формулы, возьмите хорошую книгу по математическим методам — например, Mathematical Methods for Physicists Арфкена и Вебера. Интересующие вас сведения можно взять практически из любой книги, посвященной математическим методам в физике. Вместо этого можно взять книгу по линейной алгебре, но книги по математической физике обычно более понятны и удобны. Если у нас есть матрица размером 2x2 А = аоо aoi а10 а11 то ее присоединенная матрица будет иметь такой вид: 41 101 110 г00
Математические инструменты 93 Если матрица имеет размер 3x3, например, А = 100 г01 102 а10 а11 а12 La20 v21 a22. то присоединенная матрица будет выглядеть немного сложнее: с = а11а22"а21а12 а02а2Г а01а22 а01а12" а11а02 а12а20"а10а22 а00а22"а02а20 а02а10" а00а12 ,а10а2Га11а20 а01а20"а00а21 а00аП"а01а10 Используя эту формулу при нахождении обратных матриц в программах, нужно соблюдать осторожность. Например, ее использование может вызвать проблемы, если определитель исходной матрицы будет очень маленьким числом, поэтому нужно проверять значение определителя, обращая матрицу. Кроме того, этот подход практически непригоден для больших матриц. Замечание Для обращения больших матриц, вероятно, удобнее всего использовать метод исключения Гаусса-Джордана. Сам по себе этот метод не слишком сложен, но он требует несколько более глубоких знаний о матрицах, чем те, которые можно приобрести, прочитав эту главу. Можете попробовать почитать книгу «Numerical Recipes for C++» (издательство William Press and Company) или какую-нибудь из множества других. Код методов обращения матриц из классов matrix2x2 и matrix3x3 приведен в листинге 3.15. Листинг 3.15. Методы lnverse() классов matrix2x2 и matrix3x3 1 matrix2x2 matrix2x2::Inverse() 2 3 4 5 6 7 8 9 10 11 { scalar determinant=Determinant(); if (determinant==0.0) { pmlib_error theError( "Can't invert a matrix that has a determinant of 0.") throw theError; return(
94 Глава 3 12 matrix2x2( 13 elements[1][1]/determinant, 14 -elements[0][1]/determinant, 15 -elements[1][0]/determinant, 16 elements[0][0]/determinant)); 17 } 18 19 matrix3x3 matrix3x3::Inverse() 20 { 21 scalar determinant=Determinant(); 22 if (determinant==0.0) 23 { 24 pmlib_error theError( 25 "Can't invert a matrix that has a determinant of 0.") 26 throw theError; 27 } 28 2 9 return( 30 matrix3x3( 31 // Значение элемента 0,0 32 (elements[1][1]«elements[2][2] - 33 elements[1][1]«elements[1][1])/determinant, 34 // Значение элемента 0,1 35 -(elements[0][1]«elements[2][2] - 36 elements[0][2]«elements[2][1])/determinant, 37 // Значение элемента 0,2 38 (elements[0][1]«elements[1][2] - 39 elements[0][2]«elements[1][1])/determinant, 40 // Значение элемента 1,0 41 -(elements[1][0]«elements[2][2] - 42 elements[0][2]«elements[2][1])/determinant, 43 // Значение элемента 1,1 44 (elements[0][0]«elements[2][2] - 45 elements[0][2]«elements[2][0])/determinant, 46 // Значение элемента 1,2 47 -(elements[0][0]«elements[1][2] - 48 elements[0][2]«elements[1][0])/determinant, 49 // Значение элемента 2,0 50 (elements[0][0]«elements[1][1] - 51 elements[1][1]«elements[2][0])/determinant, 52 // Значение элемента 2,1 53 -(elements[0][0]«elements[2][1] - 54 elements[0][1]«elements[2][0])/determinant, 55 // Значение элемента 2,2 56 (elements[0][0]«elements[1][1] - 57 elements[0][1]«elements[1][0])/determinant)); 58 }
Математические инструменты 95 Оба метода генерируют исключение, если определитель обращаемой матрицы равен 0. Поэтому в кодах программ вызовы этих методов нужно помещать в блоки try-catch. Итоги Математика необходима не только для физического моделирования, но и для ЗБ-графики. Она составляет солидную часть кода игр. В этой главе обсуждались геометрия, системы координат, векторы и матрицы. Математическая библиотека, созданная в этой главе - хорошая основа для работы с векторами и матрицами в двумерных и трехмерных системах координат. Однако, двигаясь дальше, мы будем изучать новые математические операции. В следующей главе мы воспользуемся приобретенными знаниями, чтобы действительно вывести что-то на экран.
Глава 4 20-преобразования и рендеринг В главе 3 «Математические инструменты» вы получили немало математических знаний, необходимых для физического моделирования и 3D- программирования. В этой главе мы сможем применить приобретенные знания в компьютерной графике и физике. 20-преобразования Предположим, что у вас есть вектор, компоненты которого вам известны применительно к какой-то системе координат. Как преобразовать этот вектор в другую систему координат? Эту задачу решают преобразования координат или трансформации. Трансформация (transformation) пересчитывает координаты вектора в одной системе координат на соответствующие координаты в другой. Обычно трансформации обозначаются заглавными буквами. Если Т - трансформация, то применение этой трансформации к вектору х записывается как Тх. Результатом этого преобразования будут координаты вектора х в новой системе координат. Предположим, что вы раздобыли древнюю карту, сообщающую, что заколдованный остров находится в 120 милях к северу и 750 милях к западу от известной вам начальной точки. Карту рисовали, пользуясь магнитным компасом, и «север» означает направление на северный магнитный полюс, а запад - направление, перпендикулярное северу. Однако ваша система GPS использует географический север - направление на северный географический полюс, через который проходит ось вращения Земли, как показано на рисунке 4.1. Чтобы найти вожделенный остров, вам придется найти способ преобразования направления в системе с «магнитным севером» в направление в системе с «географическим севером».
2Р-преобразования и рендеринг 97 Северный магнитный полюс 750 миль на запад по географическим координатам А^ ^1 г ф о -1 о •ft- S П) (-1 S о о й о <. ^ ^ о- X ш 01 ■□ Северный географический полюс Начальная точка Рис. 4.1. Различие между магнитным и географическим полюсами - причина сложностей в поиске заколдованных островов
98 Глава 4 Активные и пассивные трансформации Трансформация, которая изменяет систему координат, называется пассивной трансформацией (passive transformation), поскольку она не влияет на объекты в системе координат. Трансформация, имеющая противоположное действие, называется активной трансформацией (active transformation). Активная трансформация не изменяет координатную систему, но изменяет характеристики векторов (или других объектов) в этой координатной системе. Пассивные трансформации хорошо подходят для реализации перемещения персонажа в играх вроде Quake, использующих вид «от первого лица», то есть глазами персонажа. Сама по себе окружающая среда в них более или менее неизменна, но персонаж перемещается в ней и изменяет систему координат, в которой эта среда должна отображаться. Говоря другими словами, персонаж, представляющий вас в игре, неподвижен, если выполняется пассивная трансформация. Вы остаетесь в центре мира. Когда персонаж идет, плывет, прыгает и так далее, мир двигается вокруг вас, но сами вы не двигаетесь. В большинстве ЗБ-игр используется именно этот подход. Активные трансформации хорошо подходят для перемещения объектов в определенной системе координат, например, в имитаторах космических боев. Если вы пилотируете корабль, как, например, в старых играх серии Wing Commander, ваша точка наблюдения (ваша система координат) фиксирована, а другие корабли двигаются вокруг вас в этой системе координат. Для их перемещения используются активные трансформации. В любой реальной игре приходится использовать оба типа трансформаций. Да, вы перемещаетесь по коридорам в Quake (используя пассивные трансформации), но и те, кто бегает за вами по этим коридорам, тоже перемещаются (используя активные трансформации). Вот что самое интересное: активные и пассивные трансформации - это просто два разных подхода к одной и той же операции. Если в космосе мимо вас пролетает космический корабль (слева направо), то двигался ли он мимо вас, или вы двигались мимо него? Активную трансформацию, перемещающую объект вправо, можно заменить пассивной, перемещающей систему координат влево. В этой книге мы будем говорить о физике. Это значит, что как минимум в нескольких последующих главах мы будем обсуждать движение объектов, поэтому в большинстве случаев удобнее использовать активные трансформации. В компьютерной графике матричные преобразования используются очень часто. Большинство объектов, которые вы видите в играх, определены в виде наборов точек. Чтобы эти объекты двигались, приближались к вам и удалялись от вас, к ним применяют матричные преобразования. Матричные преобразования используются и в физике. Физика, которую мы используем в играх, - это обычно расчет сил, воздействующих на объекты. Как правило, эти силы представляются в виде векторов в некоторой системе координат. Для поворотов, масштабирования или перемещения векторов в программах используются матричные преобразования.
2Р-преобразования и рендеринг 99 Фундаментальные матричные преобразования - это перемещение (translation), поворот (rotation) и масштабирование (scaling). Рассмотрим каждое из них подробнее. Перемещение Матрицы перемещений перемещают объекты (точки, векторы, геометрические фигуры и так далее), определенные в системе координат, из одного места в другое. Как работает перемещение, показано на рисунке 4.2. Перемещение ( Дх, Ду) Рис. 4.2. Перемещение точки Выполнять перемещение довольно просто. Предположим, мы хотим переместить точку р на расстояние Дх по оси х и на расстояние Ду по оси у. Это перемещение можно записать в виде вектора t с компонентами (Дх, Ду). Если вектор v - вектор смещения для точки р, то переместить точку можно, прибавив t к v: р' = v + t Р'х = vx + tx Р'у = vy + *у Символ Д (заглавная греческая буква дельта) обычно обозначает изменение, и Дх обозначает изменение координаты х. Если компоненты р были равны (х, у) до перемещения, то после перемещения компоненты р' будут равны (х + Дх, у + Ду). Предупреждение Говоря «вверх» применительно коси у, я подразумеваю направление, в котором увеличиваются значения координаты у. Будьте внимательны - в ЗО-графике ось у может указывать куда угодно. Если вы обращаетесь непосредственно к пикселям в окне, то верхнему левому углу соответствуют координаты @, 0), а у-координаты пикселей тем больше, чем ниже эти пиксели на экране.
100 Глава 4 А что, если нам нужно обратное преобразование, то есть требуется переместить точку в исходную позицию? Это просто. Нужно отнять от получившегося вектора вектор t, и точка вернется туда, где была. Поворот Точку можно не только переместить из любой позиции в системе координат в любую другую позицию, но и повернуть вокруг любой другой точки. Однако мы не будем сразу же рассматривать вращение точки относительно любой другой точки. Сначала попробуем разобраться, как поворачивать точку относительно начала координат (см. рис. 4.3). Изображенная на рисунке точка в ходе поворота остается на одном и том же расстоянии от начала координат, но поворачивается относительно него на угол 9. Рис. 4.3. Поворот точки относительно начала координат Вопрос заключается в следующем: если у нас есть точка с вектором смещения х, которая поворачивается вокруг начала координат на угол в, то каким будет вектор смещения для ее нового положения? Посмотрите на рисунок 4.4, на котором показан вектор х, угол между двумя позициями в и новая позиция точки. Переместившаяся точка обозначена х', и ее координаты равны (х', у'). На рисунке также показаны угол <р между осью х и вектором смещения начального положения точки и радиус г. Рис. 4.4. Углы смещения при повороте Замечание В математике углы обычно отсчитываются в направлении против часовой стрелки, как на рисунке 4.4.
2Р-преобразования и рендеринг 101 Если следовать рисунку 4.4, то компоненты вектора смещения для начального положения точки будут такими: х = г cos(<p) у = г sin(p) Компоненты вектора смещения для положения точки после поворота будут следующими: х' = г cos(<p + в) у' = г sin(<p + в) А теперь нам нужно определить координаты точки после поворота, исходя из ее координат до поворота и угла этого поворота. В этом нам помогут тригонометрические тождества. Вот основные из них: sin(a + b) = sin(a) cos(b) + cos(a) sin(b) cos(a + b) = cos(a) cos(b) - sin(a) sin(b) sin(a - b) = sin(a) cos(b) - cos(a) sin(b) cos(a - b) = cos(a) cos(b) + sin(a) sin(b) sin(-a) = -sin(a) cos(-a) = cos(a) cos2(a) + sin2(a) = 1 С помощью этих тождеств формулы х' = г cos(<p + в) у' = г sin(<p + в) можно переписать в виде х' = г (cos(#>) cosF>) - sin(^>) sin@)) у' = r (sin(ip) cos(<9) + cos(<p) sin@)) Подставив в эти выражения значения х = г cos(<p) и у = г sin((p), мы получим: х' = х cos(#) - у sin@) у' = у cos@) + х sin@)
102 Глава 4 Именно это нам и было нужно - мы смогли выразить новые координаты через старые координаты и угол поворота. По выведенным нами формулам легко вычислить координаты точки после поворота. Однако эти формулы удобнее представить в виде матрицы, и чаще всего так и делается. Определим матрицу, показанную ниже: R= cos(-0) sin(-6i) -sin(-0) cos(-0) cos@) -sin@ ) sin@ ) cos@) Выполнять повороты точек можно, умножая их векторы смещения на матрицу R. Поэтому можно записать: х' = xR или в компонентном виде [х' у']=[х у] cos@ ) -sin@)' sin@ ) cos@) ОБРАТНЫЙ ПОВОРОТ А как насчет обратного поворота? Поскольку поворот выполняется с помощью матрицы, можно просто найти обратную к ней матрицу и использовать ее для выполнения обратного поворота. Обращать матрицу поворота можно, например, методом Inverse () из математической библиотеки, созданной в прошлой главе. Этот подход сработает, но давайте сначала внимательно рассмотрим матрицу R. Матрица выполняет поворот точки на угол 0. Чтобы вернуть точку в исходное положение, нужно повернуть ее на тот же угол в обратном направлении. Поэтому новая матрица вращения будет похожа на R, только вместо угла 0 в ней будет использоваться -в: cos(-e) -sin(-6)' sin(-e) cos(-6) cos(8) sin(8)' -sin(8) cos(9) Посмотрим внимательнее на компоненты обращенной матрицы. Они такие же, как и компоненты исходной матрицы R, но поменялись знаки синусов. Следовательно, можно получить обращенную матрицу вращения из исходной матрицы простым транспонированием: R-l = RT Матрица, обращенная матрица которой равна транспонированной, называется ортогональной матрицей (orthogonal matrix). Ортогональные матрицы очень удобны для компьютерных игр, поскольку транспонирование
2Р-преобразования и рендеринг 103 выполняется гораздо быстрее, чем обращение матрицы. Кроме того, транспонировать матрицу можно куда быстрее, чем заново вычислить значения тригонометрических функций. Если вам действительно нужно их вычислять, подумайте о том, чтобы применить в программе таблицу, в которой можно находить нужные значения этих функций. ПОВОРОТ ВОКРУГ ПРОИЗВОЛЬНОЙ ТОЧКИ Что, если точку нужно повернуть не вокруг начала координат, а вокруг произвольной точки? При работе с компьютерной графикой, вам, вероятно, часто будут нужны такие повороты - модели нужно вращать относительно их центров, а не относительно начала координат. Чтобы сделать это, нужно сначала перенести в начало координат точку, вокруг которой будет выполняться вращение всех точек, которые вы хотите повернуть - как показано на рисунке 4.5. Рис. 4.5. Шаг 1: перемещение центра вращения в начало координат Если а - вектор смещения от начала координат к центру вращения, а х — вектор смещения от центра вращения до поворачиваемой точки, то первый шаг в повороте точки - перемещение центра вращения в начало координат по формуле х — а При этом поворачиваемая точка сместится так, что она будет поворачиваться вокруг начала координат. Теперь можно выполнить следующий шаг - повернуть вектор смещения поворачиваемой точки с помощью матрицы поворота R, как показано на рисунке 4.6. Совмещая этот шаг с предыдущим, получаем формулу (х - a)R И, наконец, нужно переместить центр вращения (и, соответственно, поворачиваемую точку) обратно в исходную позицию, сложив ее вектор смещения с вектором а. Этот шаг показан на рисунке 4.7. Вот общая формула поворота точки относительно центра вращения а. х' = (х - a)R + a
104 Глава 4 (х - a)R Рис. 4.6. Поворот перемещенной точки относительно начала координат Рис. 4.7. Перемещение центра вращения обратно в исходную позицию Это все, что нужно для поворота точки вокруг любой другой точки. Масштабирование Масштабирование - это операция изменения размера объекта. Масштабирование выполняется куда проще поворота. Все, что нужно - умножить каждый компонент на скаляр. Например, чтобы промасштабировать вектор х, можно использовать следующую формулу: X = SX Увидеть воздействие масштабирования на отдельную точку довольно сложно, поэтому на рисунке 4.8 показано масштабирование четырех точек, расположенных вокруг начала координат, с коэффициентом 2. I I > ' 1 Рис. 4.8, Масштабирование точек с коэффициентом 2
2Р-преобразования и рендеринг 105 В данном случае скаляр, на который умножаются компоненты, называется коэффициентом масштабирования. МАТРИЧНОЕ ПРЕДСТАВЛЕНИЕ МАСШТАБИРОВАНИЯ Можно выполнять масштабирование с помощью матрицы. Помните, что такое единичная матрица I? Это матрица, для которой справедливо выражение IA = AI = А. Похожее выражение справедливо и для вектора: 1х = xl = х. Поэтому можно записать х' = sx = xs = (xl)s = x(sl) = xS Здесь S - преобразование, матрица которого есть единичная матрица, умноженная на скаляр s. Вот как она выглядит: S 0 0 s Эта матрица позволяет обобщить операцию масштабирования. Что, если компоненты вдоль диагонали матрицы будут разными? Все нормально. Это значит, что масштабирование по осям х и у будет выполняться с разными коэффициентами. В этом случае используется матрица масштабирования следующего вида: S = 0 о Разделенное по компонентам, масштабирование будет выглядеть как х' = sxx У' = syy Например, матрица s = 4 0 0 2 увеличит вдвое расстояния по оси у и увеличивает вчетверо координаты вдоль оси х, как показано на рисунке 4.9. Если умножение на 3 увеличивает длину в 3 раза, то деление на 3 должно ее втрое уменьшить. Поэтому обратная матрица матрицы масштабирования будет выглядеть так: SJ = ■1 о о 1 S„
106 Глава 4 YT Н -*- i i 1—i—i 1—i—i— —i Рис. 4.9. Результаты масштабирования при sx = 4 и sy = 2 В этом можно убедиться, перемножив эту матрицу и исходную матрицу масштабирования: SS = о 1 О — 1 О О 1 = 1 Мы получили единичную матрицу, следовательно, матрицы прямого и обратного масштабирования действительно обратны друг другу. МАСШТАБИРОВАНИЕ ОТНОСИТЕЛЬНО ПРОИЗВОЛЬНОЙ ТОЧКИ Масштабирование увеличивает или уменьшает расстояния между точками, но оно должно работать относительно некоторого центра. Представьте себе расширяющуюся сферу. Точки на краю этой сферы двигаются быстро, точки, более близкие к центру, двигаются медленнее. Точка в центре сферы совершенно неподвижна. Эта точка называется центром расширения или точкой расширения (expansion point). Точно так же, как можно поворачивать объект вокруг произвольной точки, объект можно и масштабировать вокруг произвольной точки. Это выполняется так же, как и поворот. Сначала точка расширения перемещается в начало координат. Затем выполняется масштабирование. После этого точка расширения перемещается в исходную позицию. Эта последовательность операций выражается такой формулой: х' = (х - a)S + a Здесь х - исходная позиция точки, х' - промасштабированная позиция точки, а - точка расширения, S - масштабирующее преобразование.
2Р-преобразования и рендеринг 107 Сочетание преобразований Одни преобразования можно сочетать с другими преобразованиями. Собственно говоря, мы уже это делали в этой главе. Выполняя вращение или масштабирование относительно произвольной точки, мы перемещали центр вращения или центр расширения в начало координат, выполняли операцию и перемещали центр обратно. Это практически три преобразования, объединенных в одно. Можно совместить две матрицы вращения в одну, как показано на рисунке 4.10. Предположим, что нам нужно выполнить два поворота. Назовем их Rx и R2. Пусть угол поворота Rx (в) равен 45°, а угол поворота R2 равен 30°. Эти повороты можно объединить, просуммировав углы. В результате будет выполнен поворот на 75°. R1(e = 30°) R2F = 45°) R1R2 Рис. 4.10. Объединение поворотов Если выражать это в виде матриц, нужно создать матрицу вращения для каждого из объединяемых поворотов, а потом перемножить эти матрицы. Перемножив поворачиваемую точку и полученную матрицу, мы получим требуемое новое положение точки. Затевать такую возню ради перемещения точки в двумерной системе координат нет особого смысла. Но если мы будем работать в трехмерном пространстве, то быстро поймем, почему именно так объединяются повороты во всех играх. Масштабирующие преобразования можно сочетать точно так же, как и повороты. Если промасштабировать объект сначала с коэффициентом 2, а потом еще раз - с коэффициентом 3, то результат будет тот же, что и при одном масштабировании с коэффициентом 6. Другими словами, если St = 21 и S2 = 31, то S^ = 61. Это демонстрирует рисунок 4.11. Можно объединить поворот и масштабирование. Предположим, что RS — это поворот, после которого следует масштабирование, a SR - это масштабирование, после которого следует поворот. Хотя последовательность выполнения отдельных поворотов в группе поворотов и порядок выполнения масштабирований в группе масштабирований не играет роли в двумерных системах координат, порядок выполнения комбинированных поворотов и масштабирований может оказаться важным, если sx 5* sy в масштабирующих преобразованиях. Чтобы убедиться в этом, взгляните
108 Глава 4 на рисунок 4.12, где выполняется поворот на 45° и масштабирование с коэффициентом 4 по оси х и 1 по оси у. При этом матрицы преобразований будут выглядеть так: R= cosD5) smD5) -sinD5) cosD5) 4 0 0 1 s= S1S2=61 Si =2 S2=3 ► Рис. 4.11. Объединение масштабирующих преобразований R@ = 45°) RS , ft R(( SR , S= fe?lr Рис. 4.12. Преобразования RS и SR Как видите, поворот после масштабирования и поворот до масштабирования приводят к абсолютно разным результатам. Так что в общем случае порядок преобразований имеет значение.
2Р-преобразования и рендеринг 109 Применение преобразований - вращающийся треугольник А теперь, чтобы увидеть, как все описанное применяется на практике, используем платформу физического моделирования, рассмотренную в главе 2 «Имитация ЗО-графики с помощью DirectX», для создания простой программы. Она будет отображать на экране двумерный треугольник, вращая его с помощью заданных матриц преобразований. Применение платформы физического моделирования Платформа физического моделирования облегчает процесс подготовки Direct3D к работе. Но это не значит, что все нужное будет сделано за вас. Вам придется создать проект для размещения программы в Visual Studio и настроить его конфигурацию. Затем нужно добавить код в функции платформы. СОЗДАНИЕ ПРОЕКТА Если вы создаете и выполняете примеры программ, читая эту книгу (я рекомендую вам это делать), создайте проект для примера программы. Как это сделать, зависит от того, какую версию Visual Studio вы используете. Если, например, вы используете Visual Studio 7, то для создания проекта выполните следующие действия: 1. Если Visual Studio не запущен, запустите его. Вероятно, после запуска он отобразит стартовую страницу - Start Page. 2. Если отображена стартовая страница, нажмите на ней кнопку New Project. Если нет, выберите в меню File пункт New и в открывшемся подменю выберите пункт Project. 3. Должно отобразиться диалоговое окно New Project. В списке Project Types в левой части этого окна выберите папку Visual C++ Projects. 4. В списке Templates в правой части окна выберите пункт Win32 Project. He выбирайте пункт DirectX 9 Visual C++ Wizard, если хотите использовать платформу физического моделирования. 5. Задайте имя проекта и путь к папке, в которой он должен находиться. Для нашего текущего проекта задайте имя TriSpin. Нажмите кнопку ОК. 6. В окне мастера Win32 Application Wizard перейдите на вкладку Application Settings. На этой вкладке установите флажок Empty Project. Если установить этот флажок, Visual
110 Глава 4 Studio создаст пустой проект, не загроможденный вспомогательными функциями, не нужными вашей программе. Нажмите кнопку Finish, и создание проекта закончено. Когда мастер завершит работу, Visual Studio вернется к стартовой странице. Вы создали проект, но в нем нет исходных файлов программы. Добавим к проекту файлы платформы физического моделирования. 1. Скопируйте с компакт-диска, распространяемого с книгой, файлы PMD3DApp.h и PMD3DApp.срр. Поместите их в папку только что созданного проекта. На компакт-диске эти файлы размещаются в папке Source\Chapter04\TriSpin. 2. Вернитесь в Visual Studio. В Solution Explorer щелкните правой кнопкой мыши на папке Source Files. Из появившегося контекстного меню выберите пункт Add. В открывшемся подменю выберите пункт Add Existing Item. 3. Visual Studio отобразит диалоговое окно Add Existing Item. В нем выберите файл PMD3DApp.cpp и нажмите кнопку Open. Теперь платформа добавлена в проект. Если хотите, можете повторить процедуру для файла PMD3DApp. h, щелкнув правой кнопкой на папке Header Files. Это не обязательно, но не повредит. КОНФИГУРИРОВАНИЕ ПРОЕКТА Чтобы сделать обычную Windows-программу приложением DirectX, нужно добавить в конфигурацию проекта некоторые дополнительные данные. Во-первых, нужно добавить сведения о папке, содержащей библиотечные файлы DirectX. 1. В Solution Explorer подведите указатель к имени проекта. Если вы следуете указаниям этой книги, это имя — TriSpin. Щелкните правой кнопкой мыши. В появившемся контекстном меню выберите пункт Properties. 2. Visual Studio отобразит диалоговое окно Property Pages. В списке в левой части этого окна выберите папку Linker и в раскрывшейся папке выберите пункт General. 3. В правой части окна в списке свойств выберите пункт Additional Library Directories. Справа от надписи введите: C:\DXSDK\lib Если DirectX SDK установлен не в папке С: \DXSDK, укажите папку, в которой он установлен, и ее подпапку lib. 4. В списке в левой части окна выберите пункт Input в папке Linker.
2Р-преобразования и рендеринг 111 5. Visual Studio отобразит другой список свойств. Одно из них называется Additional Dependencies. В поле справа от него введите следующий текст: di.nput8.lib d3dxof.lib dxguid.lib d3dx9dt.lib d3d9.1ib winmm.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.1ib advapi32.1ib shell32.1ib ole32.1ib oleaut32.1ib uuid.lib odbc32.lib odbocp32.1ib Щелкните на кнопке ОК. Можно избежать ручного ввода всего текста из пункта 5, скопировав этот текст из файла AdditionalDependencies. txt и вставив в поле Additional Dependencies. Этот файл хранится на компакт-диске в папке Source. ДОБАВЛЕНИЕ НУЖНЫХ ФУНКЦИЙ Последний шаг в подготовке примера программы - добавление в проект файла, содержащего функции, необходимые платформе физического моделирования: 1. Скопируйте с компакт-диска, распространяемого с этой книгой, в папку проекта файл FrameFns.cpp. Вы найдете этот файл в папке Source. 2. Переименуйте файл FrameFns.cpp соответственно имени проекта. Для этого примера можно назвать его TriSpin. срр. Теперь мы готовы приступить к написанию кода. Настройка геометрии Объекты компьютерной графики, которые вы видите на экране, обычно описываются как совокупности точек. Каждая точка описывается парой координат (х, у), если вы работаете с 20-графикой. В ЗО-графике для описания одной точки используются три координаты - (х, у, z). Точки, определяющие объекты, называются вертексами. Для нашего приложения, которое должно отображать треугольник, придется определить и треугольник, и его вертексы. В Direct3D используется формат вертексов, который Microsoft называет гибким форматом вертексов (flexible vertex format). Этот формат позволяет создать вертекс почти любого типа (в определенных пределах), какой может понадобиться вашему приложению. Для простой формы, вроде треугольника, достаточно простейшего формата вертексов с какими-то компонентами и цветом. Более сложные форматы вертексов могут хранить информацию о нормалях, материале и текстуре.
112 Глава 4 Преобразованные и непреобразованные вертексы Выполнять рендеринг в Direct3D можно двумя способами. Можно выполнить все преобразования самому и передать устройству Direct3D вертексы, которые нужно непосредственно выводить на экран. Есть и другой способ. Можно указать Direct3D, какие преобразования нужно выполнить, и передать устройству непреобразованные вертексы объекта. Direct3D выполнит заданные вами преобразования и выведет на экран их результат. Microsoft настаивала на том, что все игры должны работать с ЗО-графикой, и результатом такой настойчивости стало одно странное свойство: единственный формат преобразованных вертексов - это D3DFVF_XYZRHW. У вертексов этого формата есть четыре компонента - х, у, z и w. В этом примере у всех вертексов компоненту z будет присваиваться значение 0, а w - значение 1, чтобы мы могли сосредоточиться на 20-графике. В листинге 4.1 приведен формат вертексов, используемых в программе отображения вращающегося треугольника. Листинг 4.1. Структура вертексов для программы отображения вращающегося треугольника 1 struct vertex 2 { 3 float x, у, z; // Непреобразованная позиция 4 // вертекса в 3D-координатах 5 DWORD color; // Цвет вертекса 6 }; Тип вертекса, определенный в листинге 4.1, содержит элементы для хранения координат х, у и z, определяющих позицию вертекса. Кроме того, в типе есть элемент для хранения цвета вертекса. Отображая фигуры с помощью вертексов этого типа, Direct3D будет смешивать цвета разных вертексов. Например, как мы скоро увидим, в программе отображения вращающегося треугольника каждому вертексу треугольника задан свой цвет. Один из этих цветов - красный, а второй - синий. Для каждого пикселя треугольника между этими вертексами Direct3D определит цвет, смешивая цвета двух вертексов. Пиксели, расположенные ближе к красному вертексу, будут более красного оттенка, а расположенные ближе к синему - более синего. Чтобы Direct3D смешивал цвета вертексов, программа должна объяснить ему, что содержится в каждом вертексе. Это можно сделать с помощью набора флагов гибкого формата вертексов. Обозначения всех флагов начинаются с символов D3DFVF_, за которыми следует описание формата вертекса. В программе отображения вращающегося треугольника в вертексах хранятся их координаты (х, у, z) и цвет. Об этом и должны сообщить Direct3D флаги. В программе используются флаги D3DFVF_XYZ и
2Р-преобразования и рендеринг 113 vert { }; ex theVerteces[] = {-l.Of, -l.Of, 0. { l.Of, -l.Of, 0. { O.Of, l.Of, 0. • Of, .Of, ■ Of, OxffffOOOO,}, OxffOOOOff,}, Oxffffffff,}, D3DFVF_DIFFUSE. За дополнительной информацией о флагах обратитесь к теме D3DFVF в документации по DirectX 9.O. Чтобы упростить работу с флагами, в программе определена константа, сочетающая их. Ее определение выглядит так: #define VERTEX_TYPE_SPECIFIER (D3DFVF_XYZ | D3DFVFJ3IFFUSE) Определение формата вертексов и флаги используются программой в функции GamelnitializationO. Эта функция находится в файле TriSpin.cpp (в папке Source\Chapter04\TriSpin на компакт-диске, поставляющемся с книгой). Листинг 4.2. Функция GamelnitializationO 1 bool GamelnitializationO 2 { 3 // Инициализируем три вертекса для треугольника. 4 5 6 7 8 9 10 11 LPDIRECT3DVERTEXBOFFER9 tempPointer = NULL; 12 // Создаем вертексный буфер. 13 // Если его не удалось создать... 14 if(FAILED( 15 theApp.D3DRenderingDevice()->CreateVertexBuffer( 16 3*sizeof(vertex), 17 0, VERTEX_TYPE_SPECIFIER, 18 D3DPOOL_DEFAULT, 6tempPointer, NULL))) 19 { 20 // Пример нельзя выполнить. 21 return false; 22 } 23 else 24 { 25 // Сохраняем указатель на вертексный буфер в 26 // глобальной переменной приложения. 27 theApp.D3DVertexBuffer(tempPointer); 28 } 29 30 // 31 // Заполняем вертексный буфер. 32 // 33 34 VOID* tempBufferPointer;
114 Глава 4 35 // Блокируем доступ к нему. 36 if (FAILED( 37 theApp.D3DVertexBuffer()->Lock < 38 0, 3*sizeof(vertex), 39 (void**)StempBufferPointer,0))) 40 { 41 return false; 42 } 43 // Копируем вертексы в буфер. 44 memcpy(tempBufferPointer, theVerteces, 45 3*sizeof(vertex)); 46 // Открываем буфер. 47 theApp.D3DVertexBuffer()-MJnlock() ; 48 49 return (true); 50 } Когда программа отображения вращающегося треугольника загружается, платформа вызывает функцию GamelnitializationO. Эта функция начинается с объявления массива для хранения трех вертексов (вершин) треугольника. При определении инициализируются координаты и цвет каждого вертекса. Код определения приведен в строках 4-9 в листинге 4.2. Замечание Использование статических массивов для хранения вертексов - не самый эффективный метод использования памяти. Можно использовать динамически выделяемые массивы, но их использование усложняет алгоритмы. В нашей программе мы выводим на экран только один 20-треугольник. Объем используемой памяти незначителен, и для упрощения программы в ней используется статический массив. В играх, в которых нужно быстро обрабатывать данные, часто используются статические массивы для хранения небольших объемов данных. Мы можем использовать множество операций, которые способен выполнять Direct3D над вертексными буферами. Вертексный буфер - это область в системной памяти или видеопамяти, которая используется для пакетной обработки вертексов. Идея состоит в том, чтобы заполнить вертексный буфер вертексами и вызвать функцию, которая выполнит какие-то действия над ними - переместит, повернет или выведет на экран. Вертексный буфер в Direct3D представляет собой СОМ-объект. Используя этот буфер, ваша программа должна выполнять стандартную процедуру создания СОМ-объекта: 1. Создать переменную для хранения указателя на интерфейс объекта и присвоить ей значение NULL. 2. Вызвать функцию для создания вертексного буфера.
2Р-преобразования и рендеринг 115 Программа создает вертексный буфер, вызывая функцию Direct3D CreateVertexBuffer () в строках 14-18 листинга 4.2. Заметьте, что при вызове функции CreateVertexBuffer () используется константа VERTEX_TYPE_SPECIFIER, определенная ранее в программе. Если создание вертексного буфера прошло успешно, указатель на него сохраняется в переменной в классе d3d_app в строке 27. Затем вертексный буфер заполняется. Чтобы заполнить его, сначала нужно заблокировать доступ к нему извне, чтобы к нему могла обращаться только эта программа. Блокирование доступа к буферу необходимо при выполнении большинства операций над этим буфером. Если заблокировать доступ к буферу удалось, то функция Gamelni- tialization() копирует в него вертексы в строке 43 листинга 4.2. После этого выполняется разблокирование буфера. Обновление кадров После того, как программа выполнила инициализацию геометрической фигуры (треугольника), которую нужно отобразить на экране, начинается обработка поступающих сообщений. Кроме того, начинается обновление кадров и вывод их на экран. Программа отображения вращающегося треугольника настолько проста, что в ней не нужно обрабатывать какие-то сообщения, кроме тех, что уже обрабатывает платформа. Однако обновлять кадры необходимо. Эту операцию выполняет функция Opda- teFrame (), код которой приведен в листинге 4.3. Листинг 4.3. Функция UpdateFrame() 1 bool UpdateFrame() 2 { 3 /* Глобальная матрица будет просто вращать объект вокруг 4 начала координат в плоскости ху. */ 5 D3DXMATRIXA16 worldMatrix; 6 // Создадим матрицу вращения, чтобы выполнять 1 полный 7 // оборот B*Р1 радианов) за 1000 мс. Во избежание потери 8 // точности, свойственной очень большим числам с плавающей 9 // запятой, вычисляется остаток от деления на 1000 системного 10 // времени, и этот остаток преобразуется в угол в радианах. 11 UINT currentTime = timeGetTime() % 1000; 12 FLOAT rotationAngle = currentTime * B.Of * D3DX_PI) / 1000.Of; 13 D3DXMatrixRotationZ(SworldMatrix,rotationAngle); 14 theApp.D3DRenderingDevice()->SetTransform(D3DTS_WORLD, 15 SworldMatrix); 16 17 // Создадим матрицу отображения. Для ее определения 18 // используется позиция наблюдателя, точка, в которую 19 // направлен его взгляд, и направление, указывающее, где 20 // находится верх. В этом примере наблюдатель находится в 5 21 // единицах сзади по оси z и в 3 единицах сверху, смотрит
116 Глава 4 22 // на начало координат, а "верхом" считается направление 23 // роста координаты у 24 D3DXVECTOR3 eyePoint@.Of,3.Of,-5.Of); 25 D3DXVECTOR3 lookatPoint@.Of,0.Of,0.Of); 26 D3DXVECTOR3 upDirection@.Of,1.Of,0.Of) ; 27 D3DXMATRIXA16 viewMatrix; 28 D3DXMatrixLookAtLH(SviewMatrix, &eyePoint, SlookatPoint, 29 SupDirection); 30 theApp.D3DRenderingDevice()->SetTransfoim(D3DTS_VIEW, 31 SviewMatrix); 32 33 // Матрица проецирования в этом примере выполняет 34 //преобразование 35 // перспективы, преобразующее геометрию из 3D-представления в 36 // плоское 20-представление. Она содержит делитель перспективы, 37 // который уменьшает видимый размер далеких объектов. Чтобы 38 // создать перспективное преобразование, нужно задать поле 39 // зрения(обычно 1/4 PI), соотношение перспективы, ближнюю 40 //и дальнюю плоскости отсечения. Плоскости определяют 41 // предельные расстояния,за которыми объекты не обрабатываются. 42 D3DXMATRIXA16 projectionMatrix; 43 D3DXMatrixPerspectiveFovLH(SprojectionMatrix,D3DX_Pl/4, 44 1.Of,1.Of,100.Of); 4 5 theApp.D3DRenderingDevice() 46 ->SetTransform(D3DTS_PROJECTION,SprojectionMatrix); 47 48 return (true); 49 } При каждом вызове функция UpdateFrame () устанавливает угол поворота. Используя этот угол, она строит матрицу вращения, как показано в строке 13 листинга 4.3. Затем эта функция делает созданную матрицу глобальной матрицей преобразования (в строке 14 листинга 4.3). Что такое глобальная матрица преобразования? В ЗБ-программировании наблюдатель не перемещается в моделируемом мире. Вместо этого мир перемещается вокруг наблюдателя. Чтобы переместить наблюдателя вперед, программа сдвигает мир назад. А какое это имеет отношение к треугольнику? В программе TriSpin для создания матрицы вращения, поворачивающей треугольник в плоскости ху, вызывается функция Direct3D D3DMat- rixRotationZ (). Эта матрица сохраняется в глобальной матрице. Затем Direct3D использует глобальную матрицу для поворота всех вертексов в отображаемом программой мире. В мире программы TriSpin есть только три вертекса - это вершины треугольника. Соответственно, весь мир (один треугольник) будет вращаться в плоскости ху. Кроме глобальной матрицы, функция UpdateFrame () создает матрицу отображения и матрицу проецирования. Матрица отображения задает позицию наблюдателя в ЗБ-мире. Чтобы создать эту матрицу, программа
2Р-преобразования и рендеринг 117 использует функцию Direct3D D3DMatrixLookAtLH (). Эта функция создает матрицу отображения в левосторонней системе координат, используемой в Direct3D. Матрица проецирования добавляет перспективу в отображаемый мир, поэтому более далекие объекты будут казаться меньше по размеру. Поскольку в этой главе мы работаем с 2Б-графикой, нам нет нужды заботиться о перспективе, однако создать матрицу проецирования все же придется. Это можно сделать, вызвав функцию Direct3D D3DMatrix- PerspectiveFovLH(). Замечание В листингах, приводимых в данной книге, некоторые комментарии, присутствующие в файлах на компакт-диске, удалены, чтобы сэкономить место. В файлах на компакт-диске комментариев довольно много. Если вам нужна дополнительная информация о функциях, упомянутых в книге, обратитесь к файлам на компакт-диске или к документации по DirectX. Рендеринг кадров Чтобы выполнить рендеринг треугольника, платформа вызывает функцию RenderFrame (). Чтобы вывести треугольник на экран, эта функция должна выполнить три действия: 1. Задать Direct3D поток-источник для рендеринга. 2. Задать формат вертексов. 3. Выполнить рендеринг треугольника. Код функции RenderFrame () приведен в листинге 4.4. Листинг 4.4. Функция RenderFrame() 1 bool RenderFrame() 2 { 3 // Рендеринг содержимого вертексного буфера 4 theApp.D3DRenderingDevice() 5 ->SetStreamSource@,theApp.D3DVertexBuffer(), 6 0,sizeof(vertex)); 7 theApp.D3DRenderingDevice()->SetFVF(VERTEX_TYPE_SPECIFIER); 8 theApp.D3DRenderingDevice()-> 9 DrawPrimitive(D3DPT_TRIANGLESTRIP,0,1); 10 11 return (true); 12 } Для вывода содержимого вертексных буферов Direct3D использует потоки рендеринга. В строках 4-6 листинга 4.4 созданный функцией Game
118 Глава 4 Initialization () вертексный буфер выбирается в качестве источника потока рендеринга. Чтобы обрабатывать вертексы из этого потока, Direct3D должен знать формат этих вертексов. Этот формат задается в строке 7. В строке 8 листинга 4.4 вызывается функция Direct3D DrawPrimiti- ve (), выполняющая собственно рисование. Она выводит треугольник как группу соединенных треугольников. Вывод объектов в виде последовательностей треугольников - это обычный способ их отображения. Вертексы большинства ЗБ-объектов обычно являются вершинами треугольников, а сами ЗБ-объекты воспринимаются как совокупности треугольников. Запуск программы Чтобы скомпилировать и запустить программу, выполните следующие действия: 1. Создайте проект для нее, как описано ранее в разделе «Создание проекта» этой главы. 2. Сконфигурируйте проект, как описано ранее в разделе «Конфигурирование проекта» этой главы. 3. Скопируйте файлы программы с компакт-диска в папку только что созданного проекта. На компакт-диске эти файлы находятся в папке Source\Chapter04\TriSpin. Вам понадобятся файлы TriSpin.cpp, PMD3DApp.h и PMD3DApp.cpp. 4. В Visual Studio щелкните правой кнопкой мыши на папке Source Files в Solution Explorer. Из появившегося контекстного меню выберите пункт Add, а из открывшегося подменю - Add Existing Item. Добавьте в проект файлы . срр, скопированные в шаге 3. 5. Нажмите клавишу F5, чтобы скомпилировать и запустить программу. Если вы просто хотите увидеть программу в работе, запустите файл TriSpin.exe из папки Source\Chapter04\TriSpin на компакт-диске. Итоги В этой главе мы далеко продвинулись. Начав с элементарных понятий о матрицах и векторах, мы разобрались, как выполнять перемещения, повороты и масштабирования. Сочетая эти преобразования, мы смогли выполнять повороты и масштабирования относительно произвольных точек. Мы создали и вывели на экран простой объект. Применяя преобразования, мы заставили его двигаться, вращаясь в окне. В следующей главе мы увидим, как сделать то же самое в трехмерном пространстве.
Глава 5 ЗР-преобразования и рендеринг В этой главе мы перейдем от работы на двумерных плоскостях к работе в трехмерных пространствах. Мы разберемся, как перенести все преобразования из четвертой главы «2D-преобразования и рендеринг» в трехмерное пространство. По ряду причин эти преобразования в трехмерных пространствах более сложны. Одна из основных причин - в трехмерных сценах должна присутствовать перспектива, и объекты, более удаленные от наблюдателя, должны казаться меньше по размеру, чем такие же, но более близкие объекты. В процессе чтения этой главы вы узнаете, как перспектива влияет на рендеринг. В этой главе также показано, как создавать ЗБ-модели объектов в глобальных координатных системах и отображать их. В примерах из этой главы мы будем использовать математические преобразования, рассмотренные в предыдущих главах. Зй-преобразования С математической точки зрения обобщить преобразования в трехмерные системы координат не слишком сложно. Вспомните, что с помощью вектора можно задать любую точку в системе координат. Поэтому, если говорить о математике, переход от 2D к 3D сводится просто к добавлению еще одного измерения к векторам и матрицам. Есть только одна маленькая особенность. Чтобы операции перемножения векторов и матриц выполнялись корректно, нужно использовать однородные координаты. Однородные координаты Однородные координаты добавляют дополнительное измерение к точкам, векторам и матрицам, используемым в ваших играх. В результате программе придется выполнять дополнительные вычисления. Так зачем же использовать эти координаты?
120 Глава 5 Если ваша игра использует однородные координаты, она может умножать векторы на матрицы преобразований стандартным способом. Кроме того, можно будет объединять преобразования, преобразовывая их матрицы в одну с помощью перемножения. Чтобы использовать однородные координаты в 3D, нужно добавить к каждому вертексу в программе еще один координатный компонент. При этом точки будут определяться не как (х, у, z), а как (х, у, z, w). Компонент w - это однородная координата w. Его единственное назначение - сделать возможным представление преобразований в виде матриц. Мы будем присваивать этому компоненту значение 1. Ему можно присваивать и другие значения, но для работы над нашей книгой в этом нет необходимости. Значит ли это, что нужно добавить еще один компонент со значением 1 к координатам каждого объекта? К счастью, нет. Direct3D делает присутствие четвертой координаты практически незаметным для программиста. Для вертексов задаются координаты х, у и z, и Direct3D предоставляет функции для построения матриц преобразований, основанных на однородных координатах. Если вы приказываете Direct3D выполнить преобразование, он автоматически преобразует координаты х, у, z в х, у, z, w и перемножает их с матрицей преобразования. Но если Direct3D делает все за нас, то зачем вообще говорить о четвертой координате? В следующем разделе вы узнаете, как выполняются математические преобразования в трехмерных пространствах. Эти математические преобразования требуют применения однородных координат. Перемещение Перемещение вектора в трехмерных координатах выполняется практически так же, как и в двумерных - сложением вектора с вектором смещения t, представляющим перемещение. х" = х + t Если вернуться к главе 4, вы увидите, что в двумерных координатах для перемещения используется в точности такое же уравнение. Единственное отличие заключается в том, что вместо двумерных векторов теперь мы будем использовать трехмерные. Мы будем использовать однородную координату w в ЗБ-преобразова- ниях — каждый вектор будет состоять из 4 компонентов, а каждая матрица преобразования будет иметь размер 4x4:  0 0 0" 0 10 0 Т = 0 0 10 Ах Лу Az 1
ЗР-преобразования и рендеринг 121 Заметьте - это матрица 4x4, а не 3x3. Дополнительная строка и столбец нужны, чтобы учесть однородную координату w. Первый, второй и третий элементы слева в нижней строке содержат компоненты х, у и z вектора перемещения. Это величины смещений соответственно вдоль осей х, у и z, которые будут применяться к одному или нескольким вертексам. Чтобы переместить объект в трехмерных координатах, игра умножает каждый вертекс этого объекта на матрицу перемещения. Это делается так же, как и в двумерных координатах, но в трехмерных координатах используются вертексы с 4 компонентами, а не с 2, и матрицы преобразований размером 4x4, а не 2x2. Обратное преобразование перемещает вертекс в обратном направлении: Т~1 = 10 0 0 0 10 0 0 0 10 -Дс -Ду -Дг 1 Масштабирование Обобщить операцию масштабирования для трехмерных координат несложно. Если мы не используем однородные координаты и масштабируем объект с одинаковым коэффициентом по всем осям (то есть не сжимаем его вдоль одной оси, одновременно растягивая вдоль другой), можно по-прежнему использовать скаляр: X = SX Матричную форму тоже довольно просто обобщить. К коэффициентам масштабирования вдоль осей х и у - sx и s - добавится третий коэффициент для масштабирования по оси z - s : S = X о о о о у 0 s 0 Если используются однородные координаты, матрица будет выглядеть так: S = 5Х 0 0 0 0 sy 0 0 0 0 sz 0 0 0 0 1
122 Глава 5 Матрица обратного преобразования будет выглядеть следующим образом: 1 S = — 000 о о 0 0—0 0 0 0 1 Вращение Обобщить вращение для случая трехмерных координат несколько сложнее, чем перемещение и масштабирование. Вместо единственного способа вращения вокруг начала координат в трехмерном пространстве есть бесконечное количество таких способов, каждый из которых соответствует определенной оси вращения (см. рис. 5.1). Рис. 5.1. Бесконечное количество осей вращения, проходящих через начало координат в трехмерном пространстве Как выясняется, все вращения можно представить в виде суммы вращений по трем координатным осям. В этом можно убедиться, посмотрев на рисунок 5.2, в котором показаны три оси вращения самолета - крен, тангаж и рыскание. Нам подойдут не любые три оси (для знатоков математики поясню - эти три оси должны быть линейно независимыми), но все равно есть бесконечное количество сочетаний этих трех осей. Это очень удобно. Благодаря этой особенности мы можем выбрать любые линейно независимые оси, которые сочтем подходящими. Разумеется, если предоставить компьютеру право решать, как именно что-то сделать, ничего хорошего не получится. Поэтому выберем для работы привычный набор осей - оси х, у и z, как показано на рисунке 5.3.
ЗР-преобразования и рендеринг 123 Предупреждение Положительные направления вращения вокруг осей зависят от координатной системы. В левосторонней системе координат положительным считается направление вращения по часовой стрелке, а в правосторонней - против часовой стрелки. Рыскание Рис. 5.2. Крен, тангаж и рыскание самолета Оси на рисунке 5.3 формируют правостороннюю систему координат. Рис. 5.3. Оси вращения х, у и z Выполняя вращение на плоскости, мы практически выполняли вращение вокруг оси z, как показано на рисунке 5.4, поэтому матрицу вращения вокруг оси z можно получить, просто дополнив матрицу для двумерного вращения: R, cos(9) sin(9) О О -sin(9) cos(9) О О О 0 10 0 0 0 1
124 Глава 5 Все матрицы вращения здесь будут содержать координату w. Если вам нужны матрицы вращения без этой координаты, просто избавьтесь от нижней строки и правого столбца. Две другие матрицы вращения можно получить с помощью тригонометрии точно так же, как и первую. Рис. 5.4. Вращение на плоскости - это то же самое, что вращение в пространстве вокруг оси z Для вращения вокруг оси х служит такая матрица: R, 10 0 0 0 cos(cp) sin((p) 0 0 -sin((p) cos((p) 0 0 0 0 1 Матрица для вращения вокруг оси у выглядит так: R, cos(a) 0 -sin(a) 0 0 10 0 sin(a) 0 cos(a) 0 0 0 0 1 Выбор букв 9, ср иа для обозначения углов поворота случаен. Они просто напоминают, что углы поворотов по каждой из трех осей (и соответствующие матрицы вращения) никак не связаны друг с другом. Произвольный поворот можно представить в виде сочетания определенных поворотов вокруг трех осей координат точно так же, как любое положение самолета есть результат определенных крена, рыскания и тангажа. Чтобы упростить нам работу, повороты всегда будут выполняться в последовательности х, у, z - то есть первым будет вращение вокруг
ЗР-преобразования и рендеринг 125 оси х, вторым - вокруг оси у, а третьим - вокруг оси z. Хотя это простое правило тоже ничем не обусловлено, следуя ему, мы сможем четко и упорядочение выполнять вращения. ОБРАТНЫЕ ВРАЩЕНИЯ Вы, вероятно, будете рады узнать, что матрицы вращения в трехмерных системах координат (да и вообще в системах координат с любым количеством изменений) обращаются транспонированием. Поэтому записать обратные матрицы несложно: rcos(9) -sin(9) О О" sin(9) cosF) О О О 0 10 О 0 0 1 0 0 0" 0 cos(cp) -sin((p) 0 0 sin(cp) cos(cp) 0 0 0 0 1 cos(a) 0 sin(a) 0 0 10 0 -sin(a) 0 cos(a) 0 0 0 0 1 Если нужно применить матрицы обратных вращений, чтобы отменить поворот, их нужно применять в порядке, обратном порядку выполнения поворота. Проще говоря, обратное вращение для RXR Rz - не Rx-1RyRz, а RzRy_1Rx-1, матрицы обратных вращений используются в обратном порядке. Зй-конвейер ЗО-конвейером называется последовательность операций, которые нужно выполнить, чтобы отобразить 3D-модель на экране. Общая структура ЗО-конвейера показана на рисунке 5.5. i *;' =
126 Глава 5 Камера Проекция J Усечение тг Растеризатор Рис. 5.5. ЗО-конвейер Замечание В этой книге во всех рисунках, относящихся к ЗО-конвейеру, используется левосторонняя система координат, поскольку именно она используется в Direct3D. Коротко рассмотрим каждую стадию ЗБ-конвейера. Локальные и глобальные координаты Объекты, подаваемые на конвейер для рендеринга, обычно описаны в их собственных системах координат. Поскольку ЗБ-объекты часто называют моделями (models), локальные координаты называют также координатами моделей (model coordinates). Помещая модель в ЗБ-сцену, программа должна преобразовать вертексы модели, определенные в координатах модели, в систему координат сцены. Систему координат сцены обычно называют глобальными или мировыми координатами (world coordinates). Перенос модели из координат модели в глобальные может потребовать перемещения, масштабирования и/или поворота. В конце переноса модель будет размещена в некотором трехмерном пространстве, как показано на рисунке 5.6. В Direct3D такой перенос называется глобальным преобразованием (world transformation) модели. Как показано на рисунке 5.6, сначала модель представлена в ее собственной системе координат. Последовательность преобразований перемещает и разворачивает модель для помещения в сцену.
ЗР-преобразования и рендеринг 127 Глобальные координаты и координаты отображения Следующее преобразование в конвейере - преобразование из глобальных координат в координаты камеры, также называемые координатами отображения. В Direct3D это преобразование называется преобразованием отображения (viewing transformation). Это преобразование выбирает ось, вдоль которой ориентирована камера. Можете считать преобразование отображения пассивным преобразованием под координатную ось, локальную для камеры. Оно показано на рисунке 5.7. Локальные Глобальные Рис. 5.6. Преобразование локальных координат в глобальные • А Камера Глобальные координаты »f Рис. 5.7. Преобразование глобальных координат в координаты отображения
128 Глава 5 Преобразование сцены в координаты камеры фактически помещает камеру в начало координат. Когда камера двигается по сцене в игре, на самом деле двигается вокруг камеры сцена. Координаты отображения и координаты проецирования После преобразования сцены в координаты камеры ЗБ-конвейер преобразует ее в систему координат проецирования. На этом шаге в сцену добавляется перспектива и устанавливается поле зрения. Кроме того, этот шаг задает ближнюю и дальнюю плоскости отсечения. Объекты за дальней плоскостью отсечения и перед ближней плоскостью отсечения не отображаются. На рисунке 5.8 показана расширяющаяся область, называемая конусом видимости (viewing frustum), определяющая перспективу от ближней плоскости отсечения до дальней. Рис. 5.8. Конус видимости с ближней и дальней плоскостями отсечения Конус видимости также часто называют областью видимости. Все вертексы, не попадающие в эту область, отсекаются. Координаты проецирования и экранные координаты Чтобы вывести содержимое области видимости на экран, все, что находится в этой области, масштабируется в координаты экрана и отрисовывается в буфере - неактивной видеостранице. Процесс рисования называется растеризацией (rasterization). Масштабирование в координаты экрана необходимо, поскольку форма экрана может отличаться от формы области видимости. Например,
ЗР-преобразования и рендеринг 129 проекция области видимости на экран может быть квадратной. Однако весьма маловероятно, что экран — квадратный. Поэтому конвейер должен промасштабировать область видимости перед выводом ее на экран. Можно провести немало времени, изучая ЗБ-конвейер и, возможно, создавая собственный его вариант. Некоторые разработчики специализируются на создании конвейеров, но большинство не хочет с этим возиться. Когда вы пишете игру, вам достаточно просто понимать, как работает конвейер. Этого достаточно, чтобы использовать функции Direct3D, задающие преобразования в конвейере. Затем можно скормить сцену конвейеру Direct3D и позволить ему сделать за вас остальную работу. Замечание Когда конвейер выполняет растеризацию 30-объектов, он делает это на неактивной видеостранице. Когда растеризация заканчивается, программа делает неактивную страницу активной, а активную - неактивной. Рендеринг в 3D Познакомившись с ЗБ-преобразованиями и конвейером, вы готовы приступить к созданию собственных ЗБ-объектов и отображению их на экране. Чтобы наша первая вылазка в мир 3D была менее пугающей, мы рассмотрим две программы, основанные на программе отображения вращающегося треугольника из главы 4. Пример 1: Вращающийся треугольник в 3D В примере программы из главы 4 было показано, как описать треугольник в Direct3D. Там же демонстрировалась реализация вращения треугольника в 2D. Если вы вернетесь к главе 4 и внимательно изучите программу, то увидите, что на самом деле она работала в 3D, просто мы не обращали на это внимания. Мы проигнорировали преобразования в ЗБ-конвейере, и хотя код для выполнения глобального преобразования, преобразования просмотра и преобразования проецирования был в программе, мы его не рассматривали. Собственно говоря, при помощи инструментов вроде Di- rect3D работать с ЗО-графикой часто бывает так же просто, как и с 2D. Чтобы было проще увидеть, что программа отображения вращающегося треугольника на самом деле работает в 3D, мы модифицируем ее так, что треугольник будет вращаться вокруг осей х, у и z. Код этой программы вы найдете на компакт-диске в папке Sour- ce\Chapter05\TriangleSpin3D. В этой же папке в подпапке Bin содержится исполняемый файл этой программы TriSpin3D.exe. Если вы хотите просто запустить эту программу и посмотреть, как она работает, запустите этот файл.
130 Глава 5 ИНИЦИАЛИЗАЦИЯ ГЕОМЕТРИИ Инициализация геометрии треугольника выполняется в функции Game- Initialization (). Поскольку программа отображения вращающегося треугольника из главы 4 на самом деле работала в 3D, нам не нужно вносить в эту функцию какие бы то ни было изменения. Поэтому код функции GameInitialization() здесь не приводится. ОБНОВЛЕНИЕ КАДРОВ Для отображения процесса вращения треугольника вокруг осей х, у и z, в новой версии функции UpdateFrame () созданы три матрицы вращения. Код этой функции приведен в листинге 5.1. Листинг 5.1. Вращение в 3D 1 bool UpdateFrame() 2 { 3 D3DXMATRIXA16 worldMatrix; 4 D3DXMATRIXA16 rotationX; 5 D3DXMATRIXA16 rotationY; 6 D3DXMATRIXA16 rotationZ; 7 8 /* Вращение выполняется так же, как и в предыдущей главе. 9 Но теперь треугольник будет вращаться вокруг осей х, у и z. */ 10 UINT currentTime = timeGetTime() % 1000; 11 FLOAT rotationAngle = currentTime * B.Of * D3DX_PI) / 1000.Of; 12 D3DXMatrixRotationX(&rotationX,rotationAngle); 13 D3DXMatrixRotationY(brotationY,rotationAngle); 14 D3DXMatrixRotationZ(firotationZ,rotationAngle); 15 D3DXMatrixMultiply(SworldMatrix,SrotationX,SrotationY); 16 D3DXMatrixMultiply(SworldMatrix,SworldMatrix,SrotationZ); 17 18 /* Выбираем общую матрицу вращения в качестве глобальной 19 матрицы. */ 20 theApp.D3DRenderingDevice() -> 21 SetTransform(D3DTS_WORLD,SworldMatrix); 22 23 D3DXVECTOR3 eyePoint@.Of,3.Of,-5.Of) ; 24 D3DXVECTOR3 lookatPoint@.Of,0.Of,0.Of) ; 25 D3DXVECTOR3 upDirection@.Of,1.Of,0.Of); 26 D3DXMATRIXA16 viewMatrix; 27 D3DXMatrixLookAtLH(SviewMatrix, SeyePoint, SlookatPoint, 28 SupDirection); 2 9 theApp.D3DRenderingDevice()-> 30 SetTransform(D3DTS_VIEW,SviewMatrix); 31 32 D3DXMATRIXA16 projectionMatrix; 33 D3DXMatrixPerspectiveFovLH(&projectionMatrix,
ЗР-преобразования и рендеринг 131 34 D3DX_PI/4,1.0f,1.0f,100.0f); 35 theApp.D3DRenderingDevice() 36 ->SetTransform(D3DTS_PROJECTION, &projectionMatrix); 37 38 return (true); 39} В строках 12-15 вспомогательные функции DirectX D3DXMatrixRota- tionX(), D3DXMatrixRotationY() и D3DXMatrixRotationZ() используются для создания матриц вращения. Это матрицы 4x4, в которых используются рассмотренные нами ранее в этой главе однородные координаты. Возможно, вы поняли из названия функций, что фикция D3DXMat- rixRotationX () создает матрицу вращения вокруг оси х, а функции D3DXMatrixRotationY () и D3DXMatrixRotationZ () создают матрицы вращения вокруг осей у и z, соответственно. Замечание Вспомогательные функции DirectX не являются встроенной частью API DirectX. Это набор дополнительных функций. Для выполнения своих задач они обращаются к API DirectX. Однако использование этих функций упрощает многие аспекты работы с DirectX. Для отображения процесса вращения треугольника программе нужно объединить три матрицы вращения в одну. Функция UpdateFrame () делает это с помощью перемножения матриц. В строке 15 она вызывает еще одну вспомогательную функцию DirectX D3DXMatrixMultiply (), чтобы перемножить матрицы rotationX и rotationY и сохранить результат перемножения в матрице worldMatrix. Затем с помощью еще одного вызова функции D3DXMatrixMultiply () матрица worldMatrix умножается на матрицу rotationZ и результат записывается обратно в матрицу worldMatrix. Таким образом объединяются три матрицы вращения. Затем в строке 21 вызывается функция LPDIRECT3DDEVICE9: : Set- Transform (), которая выбирает матрицу worldMatrix в качестве глобальной матрицы. Когда DirectX выполняет рендеринг вертексного буфера, выполняются преобразования согласно глобальной матрице, которая в свою очередь преобразует локальные координаты в глобальные, как обсуждалось выше. В строке 23 задается вектор, определяющий местоположение камеры (наблюдателя) - eyePoint. Это простой способ размещения наблюдателя в ЗО-мире. В строке 22 задается еще одна точка, в направлении которой смотрит камера, - lookatPoint. Кроме того, DirectX нужно знать, какое направление считать направлением вверх при рендеринге. В строке 25 задается точка, направление на которую считается направлением вверх, — upDirection. Использование этих трех точек (или векторов) показано на рисунке 5.9.
132 Глава 5 Рис. 5.9. Три вектора, определяющих камеру Все три вектора должны храниться в матрице отображения. Вспомните - матрица отображения преобразует глобальные координаты в координаты камеры или координаты отображения. Функция OpdateFra- me () создает матрицу отображения в используемой DirectX левосторонней системе координат, вызывая вспомогательную функцию DirectX D3DXMatrixLookAtLH(). В строке 29 функция Update Frame () сохраняет матрицу отображения, чтобы DirectX применяла эту матрицу при рендеринге кадра. Последняя операция, выполняемая функцией UpdateFrame (), - подготовка матрицы проецирования. Эта матрица подготавливается в строках 32-34. Как вы, вероятно, уже догадались, для подготовки этой матрицы тоже используется вспомогательная функция DirectX. Это функция D3DXMatrixPerspectiveFovLH(). Она создает матрицу проецирования, добавляющую перспективу и определяющую поле зрения в левосторонней системе координат DirectX. Эта матрица в строке 35 сохраняется для использования при рендеринге. РЕНДЕРИНГ КАДРА Рендеринг кадра в этой версии программы выполняется точно так же, как и в версии из главы 4, поэтому код, выполняющий рендеринг, здесь не приводится.
ЗР-преобразования и рендеринг 133 Пример 2: Вращающаяся пирамида Во втором примере этой главы демонстрируется использование геометрии треугольников в 3D. В этой программе создается треугольная пирамида, вращающаяся вокруг осей х, у и z. ИНИЦИАЛИЗАЦИЯ ГЕОМЕТРИИ Конечно, создание пирамиды вместо простого треугольника требует изменения функции Gamelnitialization (). Новый код приведен в листинге 5.2. Листинг 5.2. Создание треугольной пирамиды 1 bool Gamelnitialization() 2 i 3 const int TOTAL_VERTICES = 6; 4 5 // Инициализируем 6 вертексов для рисования фигуры 6 vertex theVerteces[TOTAL_VERTICES] = 7 { -1.Of,-1.Of,0.Of,Oxffff0000,} , l.Of,-1.0f,1.0f,0xff0000ff,b 0.Of,1.Of,0.Of,Oxffffffff,}, 1.Of,-1.Of,-1.Of,OxffOOff00,}, -1.0f,-1.0f,0.0f,0xffff0000,b 1.0f,-1.0f,1.0f,0xff0000ff,}, 8 9 10 11 12 13 14 >; 15 16 LPDIRECT3DVERTEXBUFFER9 tempPointer=NULL; 17 // Создаем вертексный буфер. 18 if(FAILED(theApp.D3DRenderingDevice()->CreateVertexBuffer ( 19 TOTAL_VERTICES*sizeof(vertex), 20 0,VERTEX_TYPE_SPECIFIER, 21 D3DPOOL_DEFAULT,StempPointer,NULL))) 22 { 23 return false; 24 } 25 else 26 { 27 theApp.D3DVertexBuffer(tempPointer); 28 ) 29 30 // Заполняем вертексный буфер. 31 VOID* tempBufferPointer; 32 if (FAILED(theApp.D3DVertexBuffer()->Lock( 33 0, 34 TOTAL_VERTICES*sizeof(vertex), 35 (void**)StempBufferPointer,0)))
134 Глава 5 36 { 37 return false; 38 } 39 memcpy( 40 tempBufferPointer, 41 theVerteces, 42 TOTAL_VERTICES*sizeof(vertex)); 43 theApp.D3DVertexBuffer()->Unlock(); 44 45 return (true); 46 } Как и в предыдущих версиях нашей программы, в примере с вращающейся пирамидой используется набор треугольников. Определены б вершин, определяющих треугольники, из которых составлена пирамида. Обратите внимание, что одинаковы вертексы 1 и 5 и вертексы 2 и б. Это сделано для того, чтобы отрисовывались все стороны пирамиды. Вертексы 1, 2 и 3 образуют первую сторону. Вторая сторона образуется вертексами 2, 3 и 4, третья - вертексами 3, 4 и 5, а четвертая - вертексами 4, 5 и 6. Определив вертексы, в строках 16-21 функция Gamelnitializati- оп () создает вертексный буфер и сохраняет указатель на него в объекте d3d_app. Затем в строках 31-43 функция заполняет вертексный буфер. ОБНОВЛЕНИЕ КАДРОВ Изменения в UpdateFrame () вносить не обязательно, хотя в программе на компакт-диске вращение пирамиды и движение позиции наблюдения замедлено. В листинге 5.3 приведена новая функция UpdateFrame (). Листинг 5.3. Незначительные изменения в функции UpdateFrame() 1 bool UpdateFrame() 2 { 3 D3DXMATRIXA16 worldMatrix; 4 D3DXMATRIXA16 rotationX; 5 D3DXMATRIXA16 rotation*; 6 D3DXMATRIXA16 rotationZ; 7 8 UINT currentTime = timeGetTime() % 4000; 9 FLOAT rotationAngle = currentTime * B.Of * D3DX_PI) / 4000.Of; 10 D3DXMatrixRotationX(SrotationX,rotationAngle); 11 D3DXMatrixRotationY(SrotationY,rotationAngle); 12 D3DXMatrixRotationZ(SrotationZ,rotationAngle); 13 D3DXMatrixMultiply(SworldMatrix,SrotationX,SrotationY); 14 D3DXMatrixMultiply(SworldMatrix,SworldMatrix,SrotationZ); 15 16 theApp.D3DRenderingDevice()-> 17 SetTransform(D3DTS WORLD,SworldMatrix);
ЗР-преобразования и рендеринг 135 18 19 D3DXVECTOR3 eyePoint@.Of,3.Of,-5.Of); 20 D3DXVECTOR3 lookatPoint@.Of,0.Of,0.Of) ; 21 D3DXVECTOR3 upDirection@.Of,1.Of,0.Of); 22 D3DXMATRIXA16 viewMatrix; 23 D3DXMatrixLookAtLH(SviewMatrix, SeyePoint, SlookatPoint, 24 (upDirection); 25 theApp.D3DRenderingDevice()-> 26 SetTransform(D3DTS_VIEW,SviewMatrix); 27 28 D3DXMATRIXA16 projectionMatrix; 29 D3DXMatrixPerspectiveFovLH(SprojectionMatrix, 30 D3DX_PI/4,1.0f,1.0f,100.Of) ; 31 theApp.D3DRenderingDevice() 32 ->SetTransform(D3DTS_PROJECTION, SprojectionMatrix); 33 34 return (true); 35} РЕНДЕРИНГ КАДРА В функцию RenderFrame () внесено одно, но весьма существенное изменение. Сначала посмотрите на листинг 5.4, а потом мы разберем суть данного изменения. Листинг 5.4. Рисование треугольной пирамиды 1 bool RenderFrame() 2 { 3 // Рендеринг содержимого вертексного буфера 4 theApp.D3DRenderingDevice()->SetStreamSource( 5 0, 6 theApp.D3DVertexBuffer() , 7 0, 8 sizeof(vertex)); 9 theApp.D3DRenderingDevice()->SetFVF(VERTEX_TYPE_SPECIFIER); 10 theApp.D3DRenderingDevice()->DrawPrimitive( 11 D3DPT_TRIANGLESTRIP,0,4) ; 12 13 return (true); 14} Посмотрите на строку 11 в листинге 5.4. Вы увидите, что последний передаваемый функции DrawPrimitive () параметр теперь равен 4. Выполняя рисование с помощью этой функции, программа должна сообщить ей, сколько треугольников нужно нарисовать. В примере с вращающимся треугольником нужно было нарисовать только один треугольник. В этом примере нужно нарисовать четыре треугольника, образующих пирамиду.
136 Глава 5 ОТСЕЧЕНИЕ НЕВИДИМЫХ ПОВЕРХНОСТЕЙ В предыдущих примерах программ отображался только один треугольник. Чтобы были видны обе его стороны, в функции InitD3D () в файле PMD3DApp.cpp было отключено отсечение невидимых поверхностей. Если ваша программа отображает не пустотелые объекты, например, треугольную пирамиду, вам не нужно отображать обе поверхности каждого треугольника. Вместо этого должна отображаться только поверхность треугольника, обращенная к вам. Поэтому функция InitD3D() изменена так, чтобы отсечение невидимых поверхностей не отключалось. Новая версия кода функции приведена в листинге 5.5. Листинг 5.5. Функция lnitD3D(), не отключающая отсечение невидимых поверхностей 1 HRESULT InitD3D(HWND hWnd) 2 { 3 HRESULT hr = S_OK; 4 D3DPRESENT_PARAMETERS d3dpp; 5 6 // Создаем объект D3D. 7 if((theftpp.direct3D = Direct3DCreate9(D3D_SDK_VERSION))=NULL) 8 { 9 // Если объект создать не удалось... 10 hr = E_FAIL; 11 } 12 else 13 { 14 // Если объект D3D успешно создан... 15 // Подготавливаем структуру, используемую при создании 16 // устройства D3DDevice 17 ZeroMemory(&d3dpp,sizeof(d3dpp)); 18 d3dpp.Windowed = TRUE; 19 d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; 20 d3dpp.BackBufferFormat = D3DFMT_UNKNOWN; 21 } 22 23 // Создаем устройство D3DDevice 24 // Может ли устройство использовать HAL? 25 if ((hr==S_OK) && 26 (FAILED(theApp.direct3D->CreateDevice( 27 D3DADAPTER_DEFAULT,D3DDEVTYPE_HAL,hWnd, 2 8 D3DCREATE_HARDWARE_VERTEXPROCESSING, 29 Sd3dpp,&theApp.d3dDevice)))) 30 { 31 // Если нет, возможно, удастся использовать 32 // программную эмуляцию... 33 if(FAILED(theApp.direct3D->CreateDevice(
ЗР-преобразования и рендеринг 137 34 D3DADAPTER_DEFAULT, 35 D3DDEVTYPE_REF, 36 hWnd, 37 D3DCREATE_HARDWARE_VERTEXPROCESSING, 38 Sd3dpp, 39 StheApp.d3dDevice))) 40 { 41 // Если нет, увы... 41 hr = E_FAIL; 43 } 44 } 45 46 if (hr=S_OK) 47 { 48 /* Выключаем расчет освещения D3D, поскольку мы задаем 49 собственные цвета для вертексов.*/ 50 theApp.d3dDevice->SetRenderState(D3DRS_LIGHTING,FALSE); 51 } 52 53 return hr; 54} Если вы посмотрите на строки 46-51, то увидите, что оператор, отключавший отсечение невидимых поверхностей, удален. По умолчанию отсечение включено. Итоги В этой главе рассматривались ЗВ-преобразования, ЗБ-конвейер и рендеринг нескольких простых моделей. Мы затронули множество тем - некоторые из них мы рассмотрели подробно, некоторые — только кратко упомянули. Мы почти готовы заниматься моделированием физики, но сначала нам нужно еще кое-что сделать. В играх используются не простые модели, рассмотренные до сих пор. В них используются сложные объекты, описываемые ЗБ-сетчатыми моделями. Поэтому в следующей главе вы узнаете, как загружать и отображать сетчатые модели.
Глава 6 Сетчатые модели и Х-файлы Эта глава названа «Сетчатые модели и Х-файлы», но на самом деле она посвящена созданию моделей, выглядящих приемлемо. Моделирование физики в играх стало важным, когда достижения компьютерной графики позволили играм реалистично изображать объекты. Реалистичные объекты должны реалистично двигаться. Вам вряд ли удастся создать множество реалистичных объектов, вписывая их прямо в программы в виде нескольких строк кода на C++. Вместо этого объекты создаются в ЗБ-редакторах, например, MilkShape3D фирмы chUmbaLum sOft, 3ds max фирмы Discreet или trueSpace фирмы Caligari. Затем созданные объекты преобразуются в формат, который можно использовать в игре. Модели состоят из треугольников, поскольку треугольник - самая простая из возможных фигур на плоскости. Каждый треугольник образуется тремя вертексами. Все три вершины обязательно лежат в одной плоскости. Часть плоскости между вершинами (вертексами) треугольника называется поверхностью (face) этого треугольника. В DirectX описан формат файлов для хранения ЗО-объектов. Он называется Х-форматом. Мы будем использовать именно этот формат в нашем графическом ядре для хранения данных об объектах. Замечание Мы рассмотрим сетчатые модели, материалы и текстуры только в том объеме, который нам необходим, чтобы приступить к моделированию физики. Если вы хотите глубже изучить текстурирование (вам оно почти наверняка понадобится, чтобы создавать реалистичные игры), прочитайте другие книги, например, Mason McCuskey «Special Effects Game Programming with DirectX» (издательство Premier Press) или David Franson «The Dark Side of Game Texturing» (издательство Premier Press). Все, что мы сделаем с Х-файлами, - найдем несколько моделей и загрузим их. Можно создать Х-файл собственными силами в программе ЗО-моделирования или скачать его с сайта, на котором доступны бесплатные модели. Пример - раздел Free Stuff сайта http://www.3DCafe.com.
Сетчатые модели и Х-файлы 139 В Х-файле содержится сетчатая модель. Также в нем могут храниться текстуры и данные о материалах. Прежде чем мы сможем загрузить сетчатую модель, нужно понять, как используются текстуры и материалы. Текстуры Текстуры - это растровые рисунки, которые можно наносить на поверхность треугольника как обои. В умелых руках текстуры могут здорово улучшить вид модели. Если вы посмотрите на большинство персонажей игр, то убедитесь, что большая часть глубины и структуры этих персонажей определяется не количеством вертексов в их моделях, а использованными текстурами. Именно текстуры создают впечатление правдоподобия персонажа. Если вы умеете использовать текстуры, вы сможете добиться многого. Текстуры наносятся на сетчатую модель (mesh) с помощью текстурных координат (texture coordinates). На рисунке 6.1 показана текстура с текстурными координатами (u, v). Текстурные координаты начинаются с (О, 0) в верхнем левом углу и заканчиваются A, 1) в нижнем правом. @,0) A.0) @, 1) ппп ППП с^^^^^^^^щ^Ш^н ^^^^^^^^^^уц^д ^^^^ISSSSS^^^B A, 1) Рис. 6.1. Текстура и связанные с ней координаты Каждому вертексу в сетчатой модели присваиваются текстурные координаты. Если вы хотите, чтобы текстура растягивалась по всей поверхности многоугольника, то нужно совместить углы многоугольника и углы текстуры. Например, текстура на рисунке 6.1 растянута по всему прямоугольнику, на который она наложена. Предположим, что углы прямоугольника имеют координаты (-1.5, 1), A.5, 1), A.5, -1) и (-1.5, -1), начиная с верхнего левого и перебирая вертексы по часовой стрелке. Чтобы растянуть текстуру на весь прямоугольник, нужно задать верхнему левому вертексу текстурные координаты @, 0). При этом верхний левый угол текстуры будет совмещен с верхним левым углом прямоугольника. Верхнему правому вертексу прямоугольника нужно присвоить текстурные координаты A, 0), а нижнему правому - A, 1). И, наконец, нижнему левому вертексу нужно присвоить текстурные координаты @, 1).
140 Глава 6 Применяя текстуру, DirectX заполняет ею поверхность полигона с помощью интерполяции. Элементы растрового изображения называются пикселями (pixel), а элементы текстуры называются текселями (texel). Интерполяция сводится к назначению текселя каждой ячейке на поверхности многоугольника. @,0) @,1) A,1) Рис. 6.2. Текстурированный треугольник Посмотрите на рисунок 6.2. У изображенного на нем треугольника есть три вертекса. Верхнему вертексу присвоены текстурные координаты @, 0), поэтому на него накладывается компонент @, 0) текстуры с рисунка 6.1. У правого вертекса текстурные координаты равны A, 1), а у левого вертекса - @, 1). После задания текстурных координат текстура накладывается на поверхность с помощью интерполяции. Обратите внимание на то, как текстура искажается, когда DirectX приходится наносить квадратную текстуру на треугольный участок. Большинство текстур имеют квадратную форму. Обычно они наносятся на квадратные участки, чтобы избежать искажения. Это делается не всегда, но обычно это так. Создание текстур из файлов Если у вас есть растровый рисунок, хранящийся в файле, этот рисунок можно применить в качестве текстуры. Сначала нужно создать указатель на текстуру: LPDIRECT3DTEXTURE9 pTexture = NULL; Непосредственно создать текстуру можно с помощью функции Direct3D с довольно длинным, но легко запоминающимся названием D3DXCreate- TextureFromFile().
Сетчатые модели и Х-файлы 141 Эта функция может загружать текстуры из файлов нескольких типов, перечисленных в таблице 6.1. Заметьте, что она не поддерживает файлы .gif, .рсх и .tif. Таблица 6.1. Типы файлов, поддерживаемые функцией D3DXCreateTextureFromFile() Расширение Тип .Ьтр Растровые рисунки Windows .dds Поверхности DirectDraw . jpg Файлы изображений JPEG .png Файлы Portable Network Graphics . tga Файлы Targa Предположим, что мы хотим создать текстуру из файла lava. jpg. Это можно сделать одной строкой: D3DXCreateTextureFromFile( pDevice, "lava.jpg", pTexture ); Если вы хотите более подробно настроить процесс применения текстуры, можно воспользоваться функцией DirectX D3DXCreateTextureFrom- FileEx (). У этой функции 14 параметров! В общем, я рекомендую обходиться функцией D3DXCreateTextureFromFile (), если только у вас нет убедительных причин не делать этого. Задание текстур Любой ЗБ-игре приходится работать одновременно с множеством текстур, поэтому нужно указывать, какая текстура активна, прежде чем начинать рендеринг. Это можно сделать с помощью метода IDirect3DDevi- се9::SetTexture(). Первый параметр этой функции — индекс для текстур, позволяющий выбрать для наложения до восьми текстур. Второй параметр этой функции - указатель на созданную ранее структуру. Возможность выбора нескольких текстур используется для мулътитекстурирования (multitexturing), довольно сложного графического приема, с которым мы возиться не будем. Так что первому параметру мы присвоим значение 0. Вот, собственно говоря, и все. Задать текстуру несложно: pDevice->SetTexture( 0, pTexture );
142 Глава 6 Материалы В Direct3D материал определяет, как объект выглядит при освещении. У материала есть пять свойств, объединенных в структуру D3DMATERIAL9: typedef struct _D3DMATERIAL9 { D3DCOLORVALUE Diffuse; D3DCOLORVALUE Ambient; D3DCOLORVALUE Specular; D3DCOLORVALUE Emissive; float Power; } D3DMATERIAL9; □ Элемент Diffuse определяет цвет объекта в падающем свете. Количество отражаемого света определяется углом падения света на поверхность. □ Элемент Ambient определяет цвет объекта в рассеянном свете, то есть свете, не приходящем из отдельного четко определенного источника. Эти элементы определяют базовый цвет объекта. Обычно, чтобы соответствовать реальному миру, эти цвета должны совпадать. □ Элемент Specular определяет цвет блестящих частей объекта. Обычно этому элементу присваивается белый цвет. Увеличение значения элемента Power увеличивает резкость блестящих частей. □ Элемент Emissive определяет цвет свечения объекта. Учтите, что объект, сделанный светящимся таким образом, не будет освещать окружающие объекты. Все, что нужно, чтобы задать цвет - объявить экземпляр структуры D3DMATERIAL9, заполнить его данными и передать методу IDirect3DDe- vice9: : SetMaterial (), например: D3DMATERIAL9 Material; // Задайте цвета в структуре Material pDevice->SetMaterial( SMaterial ); Загрузка сетчатой модели Загрузить сетчатую модель довольно просто - это делает функция D3DXLoadMeshFromX (). Эта функция создает буферы для хранения материалов и текстур, которые хранятся в Х-файле. Прежде чем можно будет
Сетчатые модели и Х-файлы 143 загрузить модель, нужно объявить указатели на буфер материалов и интерфейс модели и создать переменную типа DWORD для хранения количества материалов: LPD3DXMESH theMesh; LPD3DXBUFFER materialsBuffer = NULL; DWORD nuinMaterials = 01; Теперь можно загрузить модель в память: D3DXLoadMeshFromX ( meshFileName, D3DXMESH_MANAGED, d3dDevice, NULL, & materialsBuffer, NULL, S nuinMaterials, & theMesh) ; Здесь meshFileName - имя Х-файла, в котором хранится модель. Извлечение текстур и материалов После того, как файл загружен, нужно извлечь текстуры и материалы из буфера материалов. Это необходимо, потому что буфер материалов содержит и структуру D3DMATERIAL9 с информацией о свойствах материала, и имя файла, содержащего текстуру. Чтобы извлечь текстуры и материалы из буфера материалов, нужно создать массивы для текстур и материалов: D3DMATERIAL9 *pMaterials; LPDIRECT3DTEXTURE9 *pTextures; // Создаем новые массивы текстур и материалов. pTextures = new LPDIRECT3DTEXTURE9[nuroTextures]; pMaterials = new D3DMATERIAL9[nuinMaterials]; Кроме того, нам понадобится указатель на начало буфера материалов. Этот указатель можно получить с помощью вдетода IDirect3D9: : Get- Buff erPointer(): // Получаем указатель на буфер материалов. D3DXMATERIAL* pMatBufferPointer = (D3DXMATERIAL*)pMaterialBuffer->GetBufferPointer(); Теперь можно по очереди перебрать все материалы в буфере. Общее количество материалов указано в переменной nuinMaterials. Просматривая содержимое буфера, программа заполняет массив pMaterials и
144 Глава 6 загружает текстуры в массив pTextures. Если в буфере материалов, загруженном из Х-файла, не указаны текстуры, то указатель на текстуру устанавливается в NULL: 1 // Перебираем материалы. 2 for (DWORD i = 0; i<numMaterials; i++) 3 { 4 // Подготавливаем pMaterials для извлечения текстур из буфера. 5 pMaterials[i] = pMatBufferPointer[i].MatD3D; 6 // Приравниваем цвет в рассеянном свете и цвет в свете 7 // от точечного источника. Обычно цвет в рассеянном цвете - 8 // черный, и модели выглядят слишком темными в Direct3D. 9 pMaterials[dwMatCount].Ambient = pMaterials[dwMatCount].Diffuse; 10 // Загружаем текстуры. 11 // Если текстуры есть в Х-файле... 12 if (pMatBufferPointer[dwMatCount] .pTextureFilename) 13 { 14 // Загружаем из файла. 15 if (FAILED(D3DXCreateTextureFromFile( 16 d3dDevice, 17 pMatBufferPointer[i],pTextureFilename, 18 SpTextures[i]))) 19 { 20 // Если загрузка не удалась, задаем значение NULL. 21 pTextures[i] = NULL; 22 } 23 } 24 // Если текстуры нет, задаем значение NULL. 25 else 26 { 27 pTextures[i] = NULL; 28 } 29 } Вы, вероятно, заметили в коде один интересный момент. Цвет объекта в рассеянном цвете явно задается равным цвету в свете от точечного источника. Как я уже говорил, это делается всегда, когда нужно, чтобы изображение соответствовало тому, что мы видим в реальном мире. Кроме того, иногда в загружаемых моделях задан черный цвет в рассеянном свете. При рендеринге в Direct3D такие модели выглядят очень темными. После извлечения текстур и материалов буфер материалов нам больше не нужен, и его можно освободить: pMaterialBuffer->Release();
Сетчатые модели и Х-файлы 145 Рендеринг сетчатой модели Сетчатая модель делится на части, каждая из которых характеризуется материалом и текстурой. Программа должна перебирать эти части и выполнять рендеринг каждой из них по отдельности: // Перебираем части модели по материалам, for ( DWORD i = 0; iXnumMaterials; i++ ) { // Задаем материал и текстуру для часта* модели. d3dDevice->SetMaterial(SpMaterials[i]); d3dDevice->SetTexture@,pTextures[i]); // Рисуем часть модели. pMesh->DrawSubset(i); } Функция весьма прямолинейна и проста, поскольку все сложные операции мы выполнили при загрузке модели. Осталось только выбрать материал и текстуру и приказать прорисовать часть модели. Очистка сетчатой модели Закончив работу с моделью, нужно удалить ее из оперативной памяти. Сначала удаляем массив материалов: delete[] pMaterials; Затем освобождаем интерфейсы для всех структур: for ( DWORD i = 0; KnumMaterials; i++ ) { if(pTextures[i] ) pTextures[i]->Release(); } Освободив эти интерфейсы, можно удалить массив текстур, поскольку он нам больше не нужен: delete[] pTextures; И, наконец, освобождаем интерфейс модели: pMesh->Release();
146 Глава 6 Класс d3d_mesh Вся описанная выше функциональность есть в классе d3d_mesh. Этот класс может загружать, отображать и очищать сетчатые модели. Кроме того, класс d3d_mesh поддерживает возможность, необходимую для большинства игр. Часто в играх отображается множество экземпляров одинаковых объектов. Поиграйте в трехмерную «бродилку». На стенах лабиринта есть факелы? В памяти на самом деле находится только одна модель факела, которую игра использует для отображения всех факелов. В классе d3d_mesh есть специальные элементы, позволяющие использовать в сцене одну и ту же модель много раз. Мы рассмотрим эти элементы подробно в разделе «Подсчет ссылок в классе d3d_mesh» далее в этой главе. Определение класса d3d_mesh приведено в листинге 6.1. Листинг 6.1. Класс d3d mesh 1 class d3d_mesh 2 { 3 private: 4 class mesh_data 5 { 6 public: 7 LPD3DXMESH theMesh; 8 D3DMATERIAL9 *allMaterials; 9 LPDIRECT3DTEXTURE9 *allTextures; 10 int totalMaterials; 11 12 int referenceCount; 13 14 //Public-методы. 15 mesh_data(); 16 ~mesh_data(); 17 18 }; 19 20 mesh_data *meshData; 21 22 public: 23 d3d_mesh(); 24 d3d_mesh( 25 d3d_mesh SsourceMesh); 26 ~d3d_mesh(); 27 28 d3d_mesh Soperator = ( 29 d3d_mesh SsourceMesh); 30
Сетчатые модели и Х-файлы 147 31 bool Load( 32 std::string fileName); 33 bool Render(); 34 }; Класс d3d_mesh содержит всю информацию, необходимую для загрузки и рендеринга сетчатой модели. Заметьте, что он содержит другой класс. Такой прием часто применяется в профессиональном программировании на C++, но я обычно избегаю использовать его в примерах, поскольку листинги, в которых он используется, трудно разбирать. Здесь он используется, чтобы можно было реализовать прием, известный как подсчет ссылок (reference counting). Объекты с подсчетом ссылок хранят данные точно так же, как и обычные объекты, однако они позволяют другим объектам обращаться к хранимым данным через указатель. Объекты с подсчетом ссылок подсчитывают количество объектов, указывающих на хранимые данные. При этом данные не удаляются, пока есть объекты, указывающие на эти данные. Именно эту функцию и выполняет класс mesh_data. И в классе d3d_mesh, и в классе mesh_data есть методы, необходимые им для работы. Все методы класса d3d_mesh обращаются к данным через динамически созданный объект класса mesh_data. Посмотрим, как работает класс d3d_mesh. Загрузка сетчатой модели в классе d3d_mesh Чтобы загрузить модель в объект класса d3d_mesh, нужно вызвать его метод Load (), код которого приведен в листинге 6.2. Листинг 6.2. Метод d3d_mesh::l_oad() 1 bool d3d_mesh::Load(std::string fileName) 2 { 3 bool meshLoaded=false; 4 LPD3DXBUFFER tempMaterialbuffer=NULL; 5 D3DXMATERIAL *materialBuffer=NULL; 6 7 HRESULT hr = D3DXLoadMeshFromX( 8 fileName.c_str(), 9 D3DXMESH_SYSTEMMEM, 10 theApp.D3DRenderingDevice(), 11 NULL, 12 StempMaterialbuffer, 13 NULL, 14 (DWORD *NmeshData->totalMaterials, 15 &meshData->theMesh); 16 17 // Если модель загружена...
148 Глава 6 18 if (hr=D3D_0K) 19 { 20 // Создаем буфер материалов 21 materialBuffer = 22 (D3DXMATERIAL *)tempMaterialbuffer-> 23 GetBufferPointerO ; 24 meshLoaded=true; 25 } 26 27 if (meshLoaded==true) 28 { 29 // Выделяем массив для материалов. 30 meshData-allMaterials = 31 new D3DMATERIAL9 [meshData->totalMaterials]; 32 33 // Если массив выделить не улалось... 34 if (meshData->allMaterials=NULL) 35 { 36 meshLoaded=false; 37 } 38 } 39 40 if (meshLoaded=true) 41 { 42 // Выделяем массив для текстур. 43 meshData->allTextures = 44 new LPDIRECT3DTEXTURE9 [meshData->totalMaterials]; 45 46 // Если массив выделить не удалось... 47 if (meshData->allTextures=NULL) 48 { 49 // Освобождаем массив материалов. 50 delete [] meshData->allMaterials; 51 52 meshLoaded=false; 53 } 54 > 55 56 if (meshLoaded==true) 57 { 58 for(int i=0; (i<meshData->totalMaterials);i++ ) 59 { 60 // Копируем материалы. 61 raeshData->allMaterials[i]=materialBuffer[i],MatD3D; 62 63 /* Задаем цвет материала в рассеянном цвете таким 64 же, что и в свете точечного источника. Это обычно 65 делается для реалистичного рендеринга. */ 66 meshData->allMaterials[i].Ambient =
Сетчатые модели и Х-файлы 149 67 meshData->alIMaterials[i].Diffuse; 68 69 // Если в Х-файле указано имя файла текстуры... 70 if ((materialBuffer[i].pTextureFilename != NULL) && 71 (lstrlen(materialBuffer[i].pTextureFilename) 72 > 0)) 73 { 74 // Загружаем текстуру. 75 if(FAILED(D3DXCreateTextureFromFile( 7 6 theApp.D3DRenderingDevice(), 77 materialBuffer[i].pTextureFilename, 78 SmeshData->allTextures[i]))) 79 { 80 /* Если текстуру не удалось загрузить, задаем 81 возвращаемое значение, сообщающее об ошибке. 82 */ 83 meshLoaded=false; 84 } 85 } 86 // Если текстуры нет... 87 else 88 { 89 meshData->allTextures[i] = NULL; 90 } 91 } 92 93 // Разобрались с буфером материалов. Освобождаем его... 94 tempMaterialbuffer->Release(); 95 96 } 97 return (meshLoaded); 98 } Первое, на что стоит обратить внимание в методе Load(), - данные сохраняются в объекте класса mesh_data. Как я уже говорил, это делается для подсчета ссылок, который мы рассмотрим немного позже. У метода Load () есть единственный параметр — имя Х-файла, из которого загружается модель. Объявив некоторые нужные ему переменные, метод Load() вызывает функцию D3DXLoadMeshFromX(), чтобы загрузить Х-файл (строки 7-15 листинга 6.2). Если файл успешно загружен, метод получает указатель на буфер материалов в строках 21-23. Далее метод Load() выделяет массив структур материалов в строках 30-31. Если выделение массива проходит успешно, метод перебирает все материалы в списке в строках 58-91. В теле цикла метод копирует данные о материалах из буфера в массив материалов в текущем объекте. При этом цвет в рассеянном свете задается равным цвету в свете точечного источника {строки 66-67). Если с текущим материалом связана текстура, метод
150 Глава 6 Load () пытается загрузить ее. Если это не удается, то что-то не так, и возвращается значение, свидетельствующее об ошибке. Затем это значение передается вызывающей функции. Если с материалом не связаны никакие текстуры, метод Load () просто присваивает указателю значение NULL. Предупреждение Метод d3d_mesh: :Load() может загрузить не любую сетчатую модель Di- rect3D, которая может храниться в Х-файле. Он загружает только простые сетчатые модели, которые мы будем использовать в оставшейся части книги. За дополнительной информацией об Х-файлах и загрузке моделей обратитесь к литературе. Я рекомендую книгу Wolfgang F. Engle «Beginning Direct3D Game Programming» (издательство Premier Press). Это одна из немногих книг, в которой подробно разбираются Х-файлы и их применение. Рендеринг сетчатой модели в классе d3d_mesh Рендеринг модели значительно проще, чем ее загрузка. Как уже говорилось ранее, модели состоят из частей. Поэтому, чтобы выполнить рендеринг модели, программа должна перебрать все части модели и для каждой части выполнить следующие шаги: 1. Выбрать для части материал. 2. Выбрать для части текстуру, если она задана. 3. Выполнить рендеринг части. Метод d3d_mesh: : Render (), код которого приведен в листинге 6.3, выполняет эти шаги. Листинг 6.3. Метод d3d_mesh::Render() 1 bool d3d_mesh::Render() 2 { 3 bool meshRendered=true; 4 5 /* Модели делятся на части - по одной для каждого материала. 6 Рендеринг каждой части нужно выполнять отдельно.*/ 7 for(DWORD i=0;i<(DWORD)meshData->totalMaterials;i++ ) 8 { 9 // Задаем материал и текстуру для части 10 if (theApp.D3DRenderingDevice()->SetMaterial( 11 SmeshData->allMaterials[i]) •= D3D_OK) 12 { 13 meshRendered=false; 14 } 15 16 if (theApp.D3DRenderingDevice()->SetTexture(
Сетчатые модели и Х-файлы 151 17 0,meshData->allTextures[i]) != D3D_OK) 18 { 19 meshRendered=false; 20 } 21 22 // Выполняем рисование части. 23 meshData->theMesh->DrawSubset(i); 24 } 25 26 return (meshRendered); 27) Метод Render () из листинга 6.3 выполняет три шага для рендеринга каждой части модели с помощью Direct3D. С помощью цикла, начинающегося в строке 7, перебираются все части модели. При каждом проходе цикла метод Render () вызывает функцию Direct3D LPDIRECT3DDEVICE9: : Set- Material (), чтобы задать материал части, рендеринг которой нужно выполнить. Далее, чтобы задать текстуру части, метод Render () вызывает функцию Direct3D LPDIRECT3DDEVICE9: :SetTexture (), а чтобы выполнить собственно рендеринг этой части - функцию DrawSubset (). Оптимизация в методе Render() Я целенаправленно проигнорировал возможность оптимизировать метод d3d_mesh: : Render (). Оператор if в строках 16-20 стоит поместить в еще один оператор if, проверяющий, равен ли null элемент массива allTex- tures [i]. Если да, то не нужно вызывать функцию SetTexture(), поскольку для части не задана текстура. Это ускорит выполнение рендеринга. Так почему же этого оператора if нет? Я пропустил его, поскольку заранее знал, что все модели, которые я буду использовать в книге, содержат текстуры для всех своих частей. Если у какой-то части нет текстуры, что-то не так. Кроме того, я по возможности пропускаю проверку ошибок и оптимизации в примерах из книги, если это возможно, чтобы код оставался максимально простым и понятным. Я упоминаю об этом, поскольку вы можете попытаться применить этот код в реальной игре. Если вы это сделаете, учтите, что некоторым частям моделей могут быть не назначены никакие текстуры. Если такая возможность существует, добавьте в код этот оператор if. Это позволит повысить производительность. Подсчет ссылок в классе d3d_mesh Как уже упоминалось, в игре одна и та же модель может использоваться для создания нескольких экземпляров объектов. Чтобы позволить такое использование класса d3d_mesh, в нем реализован подсчет ссылок.
152 Глава 6 Чтобы не хранить данные модели в классе d3d_mesh, данные хранят во вспомогательном классе mesh_data. В классе d3d_mesh есть динамически выделяемый объект класса mesh data. На один и тот же объект mesh_data может ссылаться несколько объектов d3d_mesh, и объект mesh_data отслеживает все объекты d3d_mesh, указывающие на него, как показано на рисунке 6.3. d3d mesh d3d mesh mesh data LPD3DMESH theMesh, D3DMATERIAL "alMatenals, LPDIRECT3DTEXTURES "alTextures, Int totalMatenals, Int referenceCount, Рис. 6.З. Множество объектов d3d_mesh, указывающих на один объект mesh_data На рисунке 6.3 показаны три объекта d3d_mesh, в которых есть указатели, изображенные в виде стрелок. Все три объекта обращаются к одним и тем же данным, и их указатели указывают на один и тот же объект mesh_data. В объекте mesh_data есть элемент referenceCount, в котором хранится количество объектов d3d mesh, ссылающихся на этот объект.
Сетчатые модели и Х-файлы 153 Чтобы понять, как выполняется подсчет ссылок, для начала еще раз посмотрим на определение класса d3d_mesh. Для удобства оно повторено ниже в листинге 6.4. Листинг 6.4. Класс d3d_mesh 1 class d3d_mesh 2 { 3 private: 4 class mesh_data 5 { 6 public: 7 LPD3DXMESH theMesh; 8 D3DMATERIAL9 *allMaterials; 9 LPDIRECT3DTEXTURE9 *allTextures; 10 int totalMaterials; 11 12 int referenceCount; 13 14 //Public-методы. 15 mesh_data(); 16 ~mesh_data(); 17 18 }; 19 20 mesh_data *meshData; 21 22 public: 23 d3d_mesh(); 24 d3d_mesh( 25 d3d_mesh SsourceMesh); 26 ~d3d_mesh(); 27 28 d3d_mesh Soperator = ( 29 d3d_mesh SsourceMesh); 30 31 bool Load( 32 std::string fileName); 33 bool Render(); 34); Определение класса mesh_data находится в private-разделе определения класса d3d_mesh. Поэтому объекты класса mesh_data могут присутствовать только в объектах класса d3d_mesh. Элементы данных класса mesh_data - это public-элементы, что довольно необычно для класса в языке C++. Элементы данных редко объявляются как public,
154 Глава 6 но этот случай - исключение из правила. Поскольку определение класса mesh_data содержится в private-разделе определения класса d3d_mesh, и поскольку объектам класса d3d_mesh нужен быстрый доступ к данным в объектах класса mesh_data, элементы данных класса mesh_data объявлены как public. Предупреждение Опытные программисты, хорошо знающие язык C++, заметят, что можно и не объявлять элементы данных класса mesh_data как public. Их можно объявить как private и создать встраиваемые (inline) public-методы доступа к ним. Использование этих методов позволит избежать снижения скорости при доступе к данным. Это правда. Компилятор C++ подставляет код встраиваемых методов в места их вызова, примерно так же, как препроцессор - макрокоманды в программах на С. Если вы реализуете класс с подсчетом ссылок более сложный, чем класс mesh_data, я настоятельно рекомендую создать public-методы доступа к его элементам данных, а сами элементы данных объявить как private. Хотя код станет несколько более объемным, он будет надежнее, а использование встраиваемых методов не приведет к снижению скорости. В классе mesh_data есть свои методы. Точнее говоря, в нем есть конструктор и деструктор. Их код приведен в листинге 6.5. Листинг 6.5. Методы класса meshdata 1 inline d3d__mesh: :mesh_data: :mesh_data() 2 { 3 theMesh=NULL; 4 allMaterials=NULL; 5 allTextures =NULL; 6 totalMaterials=0; 7 referenceCount=l; 8 } 9 10 11 inline d3d_mesh::mesh_data::~mesh_data{) 12 { 13 if (allMaterials!=NULL) 14 { 15 delete [] allMaterials; 16 } 17 18 if (allTextures!=NULL) 19 { 20 for (int i=0;i<totalMaterials;i++) 21 {
Сетчатые модели и Х-файлы 155 22 23 24 25 26 27 28 29 30 31 32 33 34 35 } } if { } if (allTextures[i]!=NULL) 1 allTextures[i]->Release() } } delete [] allTextures; (theMesh!=NULL) theMesh->Release(); theMesh=NULL; Каждый раз, создавая объект класса d3d_mesh (и, соответственно, объект класса mesh_data), игра должна инициализировать данные в объекте mesh_data значениями 0 или NULL. Эту инициализацию выполняет конструктор класса mesh_data. Кроме того, конструктор присваивает элементу referenceCount значение 1, поскольку, если объект класса mesh_data создается, то к нему обращается как минимум один объект d3d_mesh. Единственная функция, которая должна будет записывать данные в модель в этой программе, — это метод d3d_mesh: :Load(). В этой реализации данные в модели не изменяются в ходе работы программы. Это утверждение может не соответствовать истине в некоторых играх. Например, можно загрузить плоскую модель участка местности с данными о текстурах и материалах, а затем создать на этом участке холмы и впадины, изменяя координаты у отдельных вертексов. В этом случае программа должна предоставлять доступ к отдельным вертексам. В нашем примере такая возможность не нужна, и мы ее не предоставляем. При удалении объекта класса mesh_data вызывается деструктор, приведенный в строках 11-35 листинга 6.5. Деструктор выполняет операции, рассмотренные ранее в разделе «Очистка сетчатой модели». Сначала он удаляет массив материалов. Вспомните, что это массив структур, которые можно просто освободить. С другой стороны, текстуры хранятся в массиве указателей на СОМ-объекты. Поэтому деструктору приходится просматривать весь массив и вызывать функцию COM Release () для каждого элемента этого массива. Это делает цикл, приведенный в строках 20-26 листинга 6.5. После освобождения всех структур деструктор удаляет их массив в строке 27. И, наконец, деструктор освобождает саму сетчатую модель. Подсчет ссылок выполняют основной конструктор, конструктор копирования, деструктор и оператор присваивания в классе d3d_mesh. Их код приведен в листинге 6.6.
156 Глава 6 Листинг 6.6. Методы подсчета ссылок 1 inline 7 t ^ 1 3 4 5 б ) /* d3d_mesh::d3d_mesh() Сейчас функция не проверяет выделение ресурсов. Это делается позднее. */ meshData = new mesh data(); 7 8 inline 9 Ю { 11 12 13 } 14 15 in 16 17 { 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 } 39 40 in 41 { 42 43 44 45 46 47 48 49 50 } d3d mesh::d3d mesh( d3d_mesh SsourceMesh) meshData = sourceMesh.meshData; sourceMesh.meshData->referenceCount++; iline // if < > d3d mesh Sd3d mesh::operator = ( d3d_mesh SsourceMesh) Если они не одно и то же... (meshData'=sourceMesh.meshData) // Данные уходят. Уменьшим значение счетчика. meshData->referenceCount—; // Если это последний объект о'*рЯ1"я'™т>™г'вг * цяиным if (meshData->referenceCount=0) { delete meshData; } // Привязываем объект к данным. meshData = sourceMesh.meshData; // Увеличиваем значение счетчика ссылок. sourceMesh.meshData->referenceCount++; return (*this); iline // d3d mesh::~d3d mesh() Уменьшаем значение счетчика при уничтожении объекта. meshData->referenceCount—; // if { } Если это последний объект, обращавшийся к данным... (meshData->referenceCount=0) delete meshData;
Сетчатые модели и Х-файлы 157 Создавая объект класса d3d_mesh, программа вызывает конструктор этого класса. Этот конструктор приведен в строках 1-6 листинга 6.6. Все, что делает конструктор - создает объект класса mesh_data и сохраняет его адрес в элементе данных meshData. Как уже говорилось ранее, конструктор класса mesh_data инициализирует все элементы данных и присваивает элементу referenceCount значение 1. Затем программа может загружать и отображать модель. Предупреждение Чтобы сконцентрироваться на подсчете ссылок и рендеринге моделей, я не добавил в конструктор класса d3d_mesh никаких механизмов обнаружения и обработки ошибок. Никогда не поступайте так в реальных играх - это почти наверняка приведет к неработоспособности этих игр. В последующих главах мы добавим обработку ошибок в конструктор. Конструктор копирования и оператор присваивания позволят использовать одну и ту же модель нескольким объектам одновременно. Конструктор копирования прост; он создает новый объект, и нет вероятности того, что создаваемый объект уже связан с какой-то моделью. Поэтому конструктор копирования просто помещает в указатель meshData в создаваемом объекте адрес из такого же указателя в исходном объекте. При этом нужно увеличить значение в элементе referenceCount, поскольку к данным начинает обращаться еще один объект. Предположим, что программа объявляет три объекта класса d3d_mesh, названные meshl, mesh2 и mesh3. Теперь представим, что в объект meshl загружена модель. После этого выполняется такой оператор: mesh3 = mesh2 = meshl; Для выполнения этого оператора вызывается оператор присваивания класса d3d_mesh. Код этого оператора приведен в строках 15-38 листинга 6.6. Задача, стоящая перед оператором, сложнее, чем стоящая перед конструктором копирования. Во-первых, может оказаться, что объект-источник и объект-получатель — это один и тот же объект. Проще говоря, программа может содержать оператор вроде meshl = meshl; Да, таких операторов быть не должно, но, тем не менее, они встречаются. Оператор if в строке 19 листинга 6.6 предотвращает выполнение оператором присваивания всех действий, если объект-источник и объект-получатель уже обращаются к одному и тому же объекту класса mesh_data. Если объект-источник и объект-получатель обращаются к разным объектам класса mesh_data, то оператор присваивания уменьшает значение счетчика ссылок в объекте-получателе. Это необходимо сделать, поскольку после присваивания объект-получатель будет обращаться к другим данным — к тем же, к которым обращается объект-источник.
158 Глава 6 После уменьшения значения счетчика может оказаться, что к объекту класса mesh_data больше не обращается ни один объект класса d3d_mesh. Если это так, то оператор присваивания удаляет объект класса mesh_data в строках 25-28. Затем оператор присваивания записывает в указатель meshData адрес объекта класса mesh_data, на который указывает указатель meshData в копируемом объекте. Это делается в строке 31. Кроме того, он увеличивает значение счетчика ссылок, поскольку к объекту класса mesh_data теперь обращается еще один объект класса d3d_mesh. Оператор присваивания завершается как обычно — возвращая копию объекта-получателя. Единственная оставшаяся задача в подсчете ссылок - удаление объектов класса d3d_mesh. Деструктор этого класса приведен в строках 40-50. Когда удаляется объект класса d3d_mesh, он перестает обращаться к данным модели. Поэтому деструктор уменьшает значение счетчика обращений в строке 43. Если к объекту класса mesh_data больше нет обращений, этот объект тоже можно удалить. Деструктор класса mesh_data выполняет операции очистки, требуемые Direct3D. Подсказка Методика подсчета ссылок основана на подходе, описанном в статье 29 книги Scott Meyers «More Effective C++» (издательство Addison-Wesley). Эта книга и связанная с ней книга «Effective C++» (те же автор и издательство) очень полезны всем программистам, работающим на языке C++. Прочитав их вы сможете заметно повысить уровень своих познаний в C++. Итоги На компакт-диске, поставляемом с книгой, есть пример программы, показывающий, как использовать класс d3d_mesh. Он находится в папке Source\Chapter06\MeshSpin. В программе используется сетчатая модель тигра, поставляемая в составе SDK DirectX. Готовя программу к компиляции, скопируйте файлы tiger.x и tiger.bmp из подпапки SDK Media в папку проекта. При работе программа должна отображать вращающуюся модель тигра на синем фоне. Мы продвинулись от рендеринга простых объектов, состоящих из нескольких треугольников, до рендеринга сложных объектов, представленных сетчатыми моделями с текстурами. Научившись выполнять рендеринг сложных объектов, мы можем перейти к моделированию движения ЗО-объектов.
Часть II ЗР-объекты, движение и столкновения Глава 7 Динамика материальных точек Глава 8 Столкновения материальных точек Глава 9 Динамика твердых тел Глава 10 Столкновения твердых тел Глава 11 Сила тяжести и метательные снаряды Глава 12 Системы масс и пружин Глава 13 Вода и волны
Глава 7 Динамика материальных точек Эта глава посвящена одной из основных тем классической физики: движению материальных точек. Это обширная тема, и ее знание позволяет реализовать некоторые весьма впечатляющие эффекты, но это знание даст нам еще и возможность перейти к работе с твердыми и деформируемыми телами. Кроме того, в этой глав# вы познакомитесь с несколькими силами, моделирование которых нам понадобится и даст возможность сильно увеличить реалистичность игр. Кромй того, в этой главе изложены начала математического анализа. Вперед! Материальные точки Объекты, с которыми мы будем работать в этой главе, обладают одним общим свойством: они намного меньше, чем расстояния, на которые они перемещаются. Представьте себе машину, едущую по шоссе. Если рассматривать ее вблизи, как на рисунке 7.1, будет видно, что у нее есть определенная форма, есть размеры, люди могут двигаться в салоне, двери могут открываться и так далее. Во врегтя движения по дороге колеса машины вращаются. На машину действует множество сил - сила сопротивления воздуха, давление ветра, дующего на машину с одной стороны или с другой, трение между шинами и поверхностью дороги и так далее. Рис. 7.1. Машина, едущая по шоссе, выглядит сложной, если рассматривать ее вблизи
Динамика материальных точек 161 Все это можно моделировать - но это сложно. Физики и программисты, занимающиеся физическим моделированием, обычно начинают с упрощения ситуации. Первый шаг - немного отодвинуться. На рисунке 7.2 мы видим то же движение, но в более мелком масштабе. Машина выглядит почти точкой. Все маленькие элементы уже незаметны. Соответственно, положение машины, описанное вектором в некоторой системе координат, - это достаточно хорошее представление ситуации. Рис. 7.2. Та же машина, рассматриваемая издалека, может быть описана единственным вектором, определяющим ее положение в заданный момент времени Когда я говорю о материальных точках, именно такую ситуацию я и имею в виду: мы игнорируем форму объекта, его повороты вокруг осей, его размеры, вообще все, что происходит внутри этого объекта, и концентрируемся только на его свойствах, влияющих на его местоположение.
162 Глава 7 Подсказка Ключ к хорошему физическому моделированию (и быстро выполняющемуся коду) - умение определять, что нужно моделировать и вычислять, а что - нет. Одномерная кинематика Кинематика - это изучение движения при отсутствии сил. Начнем изучение кинематики с рассмотрения материальных точек в одномерной системе координат. Эти точки могут двигаться только по одной прямой. Возможно, частицы, двигающиеся по прямой при отсутствии сил, кажутся не слишком интересным предметом для изучения, но они — хорошее начало для изучения физики, а детали, которые мы пока игнорируем, могут оказаться весьма замысловатыми. Посмотрим. Скорость Изучение кинематики обычно начинается с понятия скорости. Скорость (velocity) - это расстояние, пройденное за единицу времени. Формулу для нахождения скорости можно записать так: Лх v= — At Здесь v - средняя скорость, Лх - пройденное расстояние, a At - время, затраченное на преодоление этого расстояния. Замечание Скорость измеряется в милях в час (miles per hour - mph) в США и в метрах в секунду (м/с) или километрах в час (км/ч) почти во всех других странах. Один метр в секунду - это приблизительно две мили в час. Представьте себе, что наша машина проехала мимо знака «Добро пожаловать в Канзас» и движется по длинному прямому участку дороги с приличной скоростью (см. рис. 7.3). Если вы никогда не были в Канзасе, поясню - это равнинный штат, и на его примере мы рассмотрим правдоподобный случай. Полицейский, сидящий в автомобиле за кустом в 500 метрах от границы штата, запускает секундомер, когда машина пересекает границу штата, и останавливает его, когда машина проезжает мимо куста. Полицейский видит, что прошло всего 10 секунд. Соответственно, можно найти среднюю скорость, с которой машина преодолела эти 500 метров.
Динамика материальных точек 163 v\! 1Л Рис. 7.3. Машина, мчащаяся по дороге в Канзас v = Ах / At = 500 м / 10 с = 50 м/с 50 м/с — это больше 100 миль в час, куда больше, чем ограничение скорости в штате Канзас. Полицейский чувствует себя вправе остановить машину. Скорость как производная Полицейский поправляет солнечные очки и подходит к остановившейся машине. Водитель машины вздыхает и опускает окно, начиная ритуал. Полицейский заявляет: «Сэр, знаете ли вы, с какой скоростью вы ехали?» Водитель качает головой, и полицейский продолжает: «Больше 100 миль в час». Водитель возражает: «Но я ехал всего 20 минут! Как я мог проехать 100 миль в час, если я ехал меньше часа?» Что может ответить на это полицейский? Ну, в общем-то, полицейский не обязан это делать, но знающий физику полицейский мог бы ответить: «Это значит, что если бы вы ехали с этой скоростью целый час, вы бы проехали 100 миль». Водитель отпирается: «Но я тормозил и снижал скорость, и если бы я продолжал ехать, я бы проехал меньше 100 миль!» Замечание Эта небольшая история о водителе и полицейском взята из фейнманов- ского курса лекций по физике (Фейнман Р., Лейтон Р., Сэндс М. «Фейнма- новские лекции по физике»). Его автор, Ричард Фейнман - один из величайших физиков и, возможно, величайший из преподавателей физики. Его лекции - возможно, лучший начальный учебный курс по физике.
164 Глава 7 Проблема полицейского в том, что формула для нахождения скорости позволяет найти только среднюю скорость, с которой объект прошел некоторое расстояние. Средняя скорость - это обычно не то, что мы подразумеваем, говоря о скорости. Обычно мы имеем в виду мгновенную скорость - скорость в какой-то момент времени. Взгляните на рисунок 7.4 - график расстояния, пройденного тормозящей машиной. Кривая линия обозначает расстояние, пройденное ей за интервал времени от начала измерения до выбранного момента. Поскольку вертикальная ось соответствует пройденному расстоянию, а горизонтальная ось - прошедшему времени, наклон кривой в любой точке есть скорость. Да, скорость — вектор. У нее есть величина и направление. В данном случае нас интересует величина этого вектора - положительный скаляр. t машина остановилась / * машина начала тормозить Рис. 7.4. Расстояние, пройденное время тормозящей машиной Обратите внимание, что, когда машина начинает тормозить, расстояние, которое она преодолевает за единицу времени, начинает уменьшаться. Когда машина остановилась, не важно, сколько еще пройдет времени - пройденное расстояние увеличиваться не будет. Попробуем определить скорость машины в какой-то момент времени. Первое приближение такой скорости можно получить, выбрав At, располагающееся в районе точки, для которой мы хотим узнать скорость. Взгляните на рисунок 7.5. С помощью графика мы можем найти расстояние, пройденное в начале и в конце интервала At. Разница между этими расстояниями и есть Ах. Средняя скорость в интервале времени At как раз и будет равна Ах / At. Как видите, между средней скоростью и скоростью, измеренной в какой-то момент, есть весьма существенное различие - сравните наклон исходной линии и наклон линии, которую мы использовали, чтобы получить приближенную оценку. Наклон линии равен изменению вертикальной координаты, деленному на изменение горизонтальной координаты. У горизонтальной линии изменение вертикальной координаты равно 0, и, соответственно, ее наклон равен 0. У линии с углом наклона 45° наклон равен 1, поскольку изменение вертикальной координаты равно изменению горизонтальной. У вертикальной линии наклон не определен, поскольку
Динамика материальных точек 165 ее горизонтальная координата не изменяется (деление на 0 приводит к н- еопределейному результату). В этом случае расстояние отсчитывается по вертикальной оси (это Дх). Время отсчитывается по горизонтальной оси (это At). наша оценка скорости ■ наклону = Дх / At время Рис. 7.5. Приближенная оценка скорости Возможно, вы уже сообразили, как получить более точную оценку скорости - нужно уменьшить At. Этот прием очень хорошо работает: посмотрите на рисунок 7.6. время Рис. 7.6. Чем меньше At, тем точнее оценка скорости Можно повторять процесс раз за разом, уменьшая At и получая все более и более точные оценки мгновенной скорости. Этот метод мы будем использовать во многих наших физических моделях. Решения можно сделать более точными, применяя более мелкие шаги. Вас, возможно, интересует, что произойдет в предельном случае, когда At стремится к 0. Этот случай записывается так: = lim — At->o At
166 Глава 7 Результатом этого выражения является как раз та мгновенная скорость, которая нам нужна. Бесконечно малое At записывается как dt, a соответствующее ему бесконечно малое Ах - как dx. Используя эти обозначения, можно записать: Ах dx v = hm — = — At-*0 At dt Величина dx / dt называется «производной от х по t». Процесс вычисления значения производной называется дифференцированием. Замечание Это очень краткое и сжатое введение в дифференциальное исчисление. Если вы хотите погрузиться глубже в математику, возьмите любую книгу по началам математического анализа. Существует множество хороших книг на эту тему. Можно порекомендовать, например, следующие: Gerald Bradley, Karl Smith «Calculus» и Victor Bryant «Yet another introduction to analysis»'. Часто самого процесса дифференцирования можно избежать, применив хорошую численную аппроксимацию, легко реализуемую в коде. Ускорение Помните машину во время торможения? Ее скорость постепенно падает, то есть изменяется с течением времени. Ускорение - это мера изменения скорости во времени, точно так же, как скорость - мера изменения пройденного расстояния во времени. Соответственно, среднее значение ускорения можно найти по формуле: Av а = — At Здесь а есть среднее значение ускорения, Av — изменение скорости, а At — интервал времени, за который произошло это изменение. Ускорение измеряется в м/с2. Вот пример. Представим себе, что ваш спортивный автомобиль может разогнаться от 0 до 32 м/с за 4 секунды. Тогда среднее ускорение за эти четыре секунды будет равно: а = C2 м/с) / 4 с = 8 м/с2 А теперь попробуем сделать в некотором смысле обратную операцию. Предположим, что в течение 5 секунд вы набираете скорость с ускорением 4 м/с2. Преобразовав уравнение 1 Отечественному читателю доступнее Фихтенгольц Г. М. «Курс дифференциального и интегрального исчисления» и Смирнов В. И. «Курс высшей математики» . Обе книги переиздавались много раз и выложены в Интернете, например, на сайте lib.mexmat.ru. — (Прим. перев.).
Динамика материальных точек 167 Av а = — At к виду v = aAt мы получим v = D м/с2)E с) = 20 м/с Мгновенное значение ускорения можно получить, взяв лимит при At, стремящемся к нулю: Av dv а = lim — = — *-»о At dt Мгновенное значение ускорения - это значение ускорения в конкретный момент времени. Силы Большая часть древних мыслителей Запада считала, что движущиеся объекты постепенно останавливаются. Это утверждение, на первый взгляд, подтверждается практикой. Если вы хоть раз передвигали мебель, вы готовы будете поклясться, что движение в данный момент времени не гарантирует движения в последующие моменты. Однако у древних мыслителей были сомнения в правильности этой теории. Они видели несоответствие теории и практики и пытались выяснить причины этих несоответствий. Галилей предложил хорошую альтернативу этой теории, назвав ее принципом инерции. Его теория утверждает, что объект, движущийся по прямой линии с постоянной скоростью, будет продолжать двигаться вечно, если на него не действуют другие объекты. Создание такой теории потребовало хорошего воображения - почти все предметы вокруг нас постепенно останавливаются, если их постоянно не подталкивать. Однако Галилей понял, что это постепенное замедление движения вызвано трением между объектами, а не свойственно самим объектам. Сэр Исаак Ньютон позднее сформулировал ту же идею в более общей форме. Эта форма стала известна как первый закон Ньютона. Второй закон Ньютона стал ответом на следующий логичный вопрос. Если объекты, на которые ничто не действует, будут двигаться с постоянной скоростью вечно, то, что же происходит с объектами, на которые что-то действует? Ответ - сила, действующая на объект, равна произведению массы этого объекта на ускорение, с которым он движется: F = та У большинства объектов есть определенная масса — мера того, насколько сложно изменить скорость этих объектов. Чем тяжелее и массивнее
168 Глава 7 объект, тем большее усилие надо приложить к нему, чтобы изменить его скорость на требуемую величину. Эта формула подразумевает, что масса объекта не изменяется с течением времени. Например, с помощью этой формулы мы можем рассчитать силы, действующие на движущийся по дороге автомобиль. Но масса автомобиля должна оставаться постоянной. Вы можете заметить, что она изменяется - автомобиль сжигает горючее, чтобы двигаться. Вы правы. Однако это изменение малозаметное, и его можно было игнорировать в играх. Большинство людей запоминают второй закон Ньютона именно в виде формулы F = та, потому что эта формула весьма полезна на практике. Если у нас есть сила и масса, мы можем найти ускорение. Если мы можем найти ускорение, мы можем найти скорость, и, зная скорость, мы можем вычислить пройденное расстояние. Замечание Возможно, вы слышали, что в специальной теории относительности масса объекта зависит от его скорости. Не беспокойтесь об этом. Если только вы не пытаетесь моделировать специальную теорию относительности в своей игре (я не знаю ни одной игры, в которой это делается), можно считать массу объекта не зависящей от его скорости. Единственная сложность - учесть все силы, действующие на объект. В современной физике считается, что есть четыре вида сил - гравитационные, электромагнитные, сильного и слабого взаимодействия. Теоретически, учтя все эти силы, мы сможем просчитать все, что будет происходить с объектом. Но на практике это почти невозможно сделать. На практике мы измеряем силы, действующие на объект в нескольких ситуациях, и выбираем уравнение, хорошо описывающее действие этих сил в определенных условиях. Например, если мы растягиваем пружину, то заметим, что она противодействует растяжению - на нас действует сила, как показано на рисунке 7.7. F = -к (х - х0) < Г^У~У~^^\ Рис. 7.7. Сила в растянутой пружине а *0 1 X
Динамика материальных точек 169 Действующую на нас силу можно смоделировать уравнением F = -к(х - х0) Здесь х есть координата точки в конце пружины, а х0 - координата этой же точки, когда пружина не растянута и не сжата, к - это константа, называемая коэффициентом жесткости пружины. Это уравнение показывает, что чем больше разница между х и х0, тем больше действующая на нас сила. Это утверждение соответствует нашему практическому опыту - чем сильнее растянута пружина, тем труднее растянуть ее еще больше. Значение к зависит от жесткости пружины. Двумерная и трехмерная кинематика Все уравнения, которые мы только что рассмотрели, легко можно применить и для многомерных моделей. Взгляните на наше первое кинематическое уравнение: dx х dt Если х - расстояние, пройденное вдоль оси х в двумерной декартовой системе координат, то это уравнение описывает скорость движения материальной точки вдоль оси х, как показано на рисунке 7.8. Движение вдоль оси у можно рассматривать независимо от движения вдоль оси х: Vy dt Если нам нужно работать в трех измерениях, точно так же можно записать и скорость движения вдоль оси z: dz v = — z dt А теперь подумаем. Если v - вектор, компоненты которого в 2D есть (vx, vy), а вектор х - вектор с компонентами (х, у), то можно объединить два уравнения: dx v= — dt
170 Глава 7 у I Рис. 7.8. Скорости вдоль осей х и у В компонентной форме это будет выглядеть как vx Л. d X LyJ dt ~dx~ dt dy L dt J Теперь, когда у нас есть уравнение в векторной форме, мы можем гарантировать, что оно будет работать в любом количестве измерений и в любой координатной системе. В трехмерной системе координат оно будет выглядеть так же: dx v= — dt Компонентная его форма будет такой: г -| vx У LvzJ d X У z dt "dx dt dy dt dz dt У вектора скорости, как и у любого другого вектора, есть величина и направление. Направление этого вектора - это направление движения материальной точки, а величина - скорость движения, как показано на рисунке 7.9:
Динамика материальных точек 171 скорость = |v| = v скорость Рис. 7.9. Вектор скорости материальной точки можно представить в виде направления и модуля скорости Можно переписать в векторной форме все остальные уравнения, которые мы использовали в предыдущем разделе. Они будут выглядеть так: dx v= — dt dv a= — dt F = ma Эти уравнения выражают скорость, ускорение и силу в виде векторов. Вы можете спросить: «Ну и что?» Дело в том, что при программировании ЗБ-сцен, содержащих множество движущихся объектов, часто можно просчитывать движение объектов, воспринимая их как материальные точки. Такое упрощение позволяет очень просто находить линейную скорость и ускорение каждого объекта, а также силы, действующие на них. Если скорости, ускорения и силы можно представлять в виде векторов, то их можно представлять и в виде матриц. И опять вы можете спросить: «Ну и что?» Вспомните, что все ЗБ-объекты в программах представляются в виде сетчатых моделей, представляющих собой совокупности вертексов. Когда объект двигается в ЗБ-сцене, двигаются все вертексы его модели. Это значит, что игра должна применять концепции скорости, ускорения и силы к каждому вертексу в модели. А ведь сложные модели состоят из сотен тысяч треугольников - и каждый треугольник состоит из трех вертексов.
172 Глава 7 Как же программа может рассчитывать движение каждого вертекса в таком сложном объекте? А она этого и не делает. Программа воспринимает объект как материальную точку и использует векторы для представления сил, которые действуют на этот объект, чтобы вычислить его скорость и ускорение. Затем эти скорость и ускорение представляются в виде матриц. После этого перемещение всех вертексов в объекте сводится к операции матричного умножения для каждого вертекса - и объект движется реалистично, так же, как и в реальной вселенной. Моделирование материальных точек Теперь, когда у нас есть физические соотношения, которые нам требовались, мы можем написать код, моделирующий материальные точки в программе. Сначала мы создадим класс для представления материальных точек. Затем мы поместим материальную точку в среду, в которой нет ни силы тяжести, ни трения. Это позволит нам понаблюдать за поведением материальных точек, на которые действует только одна внешняя сила. В последующих главах вы узнаете, как реализовать среды, в которых присутствуют сила тяжести и трение. Пример программы будет отображать шарик, движущийся слева направо по окну. Поскольку мы пока не рассматриваем кинематику вращения, то вращаться в этой программе шарик не будет. Класс d3d_point_mass Класс, представляющий материальную точку, должен хранить данные о массе этой точки, ее местоположении и действующих на нее силах. В листинге 7.1 приведено определение такого класса - класса d3d_point_niass. Замечание Код примера программы из этой главы вы можете найти на компакт-диске, поставляемом с книгой. Он находится в папке Source\Chapter07\Point- Mass. Если вы хотите просто посмотреть на программу в работе, исполняемый файл этой программы находится в папке Source\Chapter07\Bin. Листинг 7.1. Определение класса d3d_point_mass 1 class d.3d_point_mass 2 { 3 private: 4 d3d_mesh objectMesh;
Динамика материальных точек 173 5 6 scalar mass; 7 vector_3d centerOfMassLocation; 8 vector_3d linearVelocity; 9 vector_3d linearAcceleration; 10 vector_3d sumForces; 11 12 D3DXMATRIX worldMatrix; 13 14 public: 15 d3d_point_mass(); 16 17 bool LoadMesh( 18 std: -.string meshFileName) ; 19 20 void Mass( 21 scalar massValue); 22 scalar Mass(void); 23 24 void Location( 25 vector_3d locationCenterOfMass); 26 vector_3d Location(void); 27 28 void LinearVelocity( 29 vector_3d newVelocity); 30 vector_3d LinearVelocity(void); 31 32 void LinearAcceleration( 33 vector_3d newAcceleration); 34 vector_3d LinearAcceleration(void); 35 36 void Force( 37 vector_3d sumExternalForces); 38 vector_3d Force(void); 39 40 bool Update( 41 scalar changelnTime); 42 bool Render(void); 43 }; В классе d3d_point_mass определены private-элементы данных, в которых хранятся сетчатая модель объекта, масса, координаты центра масс и характеристики движения. Кроме того, в нем есть специальный элемент данных, предназначенный для работы с Direct3D. Если вы посмотрите на строку 12 листинга 7.1, вы увидите объявление элемента с именем worldMatrix. Это матрица глобального преобразования или глобальная матрица, которая рассматривалась в главе 4 «2Б-преобразования и рендеринг». Direct3D использует глобальную матрицу для обновления
174 Глава 7 расположения и ориентации объектов в трехмерном пространстве. Если в вашей программе используется множество объектов класса d3d_po- int_mass, движущихся по сцене, то для просчета движения каждого такого объекта понадобится своя глобальная матрица. Именно она и хранится в элементе worldMatrix объекта и считывается из него при вызове метода Update () в программе. В методе Render () эта матрица используется для просчета перемещения материальной точки. Кроме private-элементов данных, в определении класса объявлены public-методы этого класса (строки 15-42 листинга 7.1). Большая часть этих методов просто считывает или записывает значения элементов данных. Основную работу в классе выполняют методы LoadMesh (), Update () и Render (). Метод LoadMesh () настолько прост, что объявлен как встраиваемый в файле PMPointMass. h. Код этого метода приведен в листинге 7.2. Листинг 7.2. Метод LoadMeshQ 1 inline bool d3d_point_mass::LoadMesh( 2 std::string meshFileName) 3 { 4 assert(meshFileName.length()>0); 5 6 return (objectMesh.Load(meshFileName)); и Этот метод загружает сетчатую модель объекта, определяющую его внешний вид, с помощью метода d3d_mesh: :Load(). Подсказка В строке 4 листинга 7.2 метод LoadMesh () использует макрос assert (), чтобы гарантировать, что имя файла имеет ненулевую длину. Если программа вызовет этот метод, передав ему пустое имя файла, то выполнение программы аварийно завершится. Ошибка такого рода - это скорее ошибка программиста, чем ошибка времени выполнения. Макрос assert {) гарантирует, что все ошибки такого рода будут устранены, прежде чем программа будет выпущена. Вообще говоря, он очень удобен для защиты от ошибок программистов в параметрах вызова функций. Методу Update () приходится работать больше. Его код приведен в листинге 7.3. Метод Update () класса d3d_point_mass просчитывает линейную динамику материальной точки. Пока он игнорирует существенные моменты реального мира - вращение, трение и силу тяжести. Однако эта его версия позволит в будущем учесть все эти моменты. Метод Update () начинается с проверки массы материальной точки. Она должна быть ненулевой. Это важная проверка, поскольку она позволяет обнаруживать часто встречающуюся ошибку программирования. Масса не должна быть нулевой или отрицательной - это невозможно физически.
Динамика материальных точек 175 Листинг 7.3. Метод Update() 1 bool d3d_point_mass::Update( 2 scalar changelnTime) 3 { 4 // 5 // Начинаем просчет линейкой динамики. 6 // 7 8 // Находим линейное ускорение. 9 // а = F/m 10 assert(mass!=0); 11 linearAcceleration = sumForces/mass; 12 13 // Находим линейную скорость. 14 linearVelocity += linearAcceleration * changelnTime; 15 16 // Определяем новое местоположение центра масс. 17 centerOfMassLocation += linearVelocity * changelnTime; 18 19 // 20 // Просчет линейной динамики закончен. 21 // 22 23 // Создаем матрицу преобразования. 24 D3DXMatrixTranslation( 25 SworldMatrix, 26 centerOfMassLocation.X(), 27 centerOfMassLocation.Y(), 28 centerOfMassLocation.Z()); 29 30 return(true); 31} П реду п режден ие Выполняя физическое моделирование, мы часто игнорируем массу некоторых объектов в системе. Это позволяет упростить вычисления настолько, чтобы их можно было выполнить. Если вы используете этот прием, не применяйте объекты класса d3d_point_mass для реализации не имеющих массы объектов - они для этого не предназначены. В строке 11 листинга 7.3 метод Update () преобразует формулу F = та к виду а = F / т, чтобы найти ускорение материальной точки. В каждом кадре игры нужно просчитывать воздействие сил на материальную точку. Затем вызывается метод d3d_point_mass: : Force () для задания суммы всех сил. Когда программа вызывает метод d3d_point_mass: :Update (), этот метод вычисляет реакцию материальной точки на воздействие силы.
176 Глава 7 Затем метод Update () использует ускорение, чтобы найти новую линейную скорость объекта в конце интервала времени, заданного параметром changelnTime. В строке 17 метод Update () использует скорость (и интервал времени), чтобы найти новое местоположение центра масс объекта. Вычисления в строках 11, 14 и 17 весьма просты, поскольку мы используем для их выполнения инструменты, созданные в главе 3 «Математические инструменты». Использование векторов делает формулы расчетов простыми. Следующий шаг - преобразование в матричную форму. Оно необходимо, чтобы программа могла использовать преобразования, рассмотренные в главах 4 и 5. Преобразование в матричную форму начинается в строке 24 листинга 7.3. В строках 24-28 метод Update () вызывает функцию Direct3D D3DXMat- rixTranslation(), чтобы создать матрицу перемещения. Эта функция сохраняет матрицу перемещения в матрице глобального преобразования объекта класса d3d_point_mass. Матрица глобального преобразования используется при вызове метода d3d_point_mass: :Render (), код которого приведен в листинге 7.4. Листинг 7.4. Метод Render() 1 bool d3d_point_mass::Render(void) 2 { 3 // Сохраняем матрицу глобального преобразования. 4 D3DXMATRIX saveWorldMatrix; 5 theApp.D3DRenderingDevice()->GetTransform ( 6 D3DTS_WORLD, 7 «saveWorldMatrix); 8 9 // Применяем к объекту свою матрицу глобального 10 // преобразования. 11 theApp.D3DRenderingDevice{)->SetTransform( 12 D3DTS_WORLD,SworldMatrix); 13 14 // Выполняем рендеринг объекта с учетом выполненных 15 // преобразований. 16 bool renderedOK=objectMesh.Render(); 17 18 // Восстанавливаем матрицу глобального преобразования. 19 theApp.D3DRenderingDevice()->SetTransform( 20 D3DTS_WORLD, 21 SsaveWorldMatrix); 22 23 return (renderedOK); 24) Для рендеринга одного объекта класса d3d_point_mass методу Render () нужна только глобальная матрица для этого объекта. Эта
Динамика материальных точек 177 матрица нужна, чтобы определить местоположение объекта в 3D-npo- странстве. Однако в Direct3D может существовать одновременно только одна глобальная матрица. Поэтому методу Render () приходится выполнять следующие действия: 1. Сохранить ранее использовавшуюся глобальную матрицу. 2. Выбрать глобальную матрицу объекта класса d3d_j>oint_mass как используемую в данный момент. 3. Выполнить рендеринг модели объекта класса d3d_point_mas s. 4. Восстановить ранее использовавшуюся глобальную матрицу, сохраненную в шаге 1. С помощью этих шагов метод Render () позиционирует объект в ЗБ-пространстве и выполняет рендеринг этого объекта, не повреждая ранее использовавшуюся глобальную матрицу. Если бы метод Render () не сохранял и не восстанавливал ранее использовавшуюся матрицу, то перемещение, примененное к объекту класса d3d_point_mass, применялось бы и к объектам, рендеринг которых выполнялся бы после рендеринга этого объекта. Результаты были бы как минимум нежелательными. В листинге 7.4 показано, что метод Render () класса d3d_point_mass последовательно выполняет перечисленные в списке выше действия. Сначала объявляется временная переменная для хранения ранее использовавшейся глобальной матрицы. Затем с помощью функции Direct3D GetTransform() глобальная матрица сохраняется в этой переменной. В строках 11-12 матрица, хранящаяся в объекте класса d3d_point_mass, выбирается в качестве глобальной. Затем выполняется рендеринг - для этого в строке 16 вызывается метод d3d_mesh:: Render (). В строках 19-21 восстанавливается ранее использовавшаяся глобальная матрица. Применение класса d3d_point_mass Итак, теперь у нас есть готовый к применению класс d3d_point_mass. Давайте используем его в программе. Но прежде чем мы это сделаем, я хочу немного поговорить об освещении в Direct3D. Мы воспользуемся некоторыми возможностями Direct3D по моделированию освещения, чтобы получить в примере программы реалистично выглядящий шар. ВКЛЮЧЕНИЕ МОДЕЛИРОВАНИЯ ОСВЕЩЕНИЯ В DIRECT3D Игре можно заметно добавить реалистичности, используя возможности моделирования освещения, присутствующие в Direct3D. Мы могли бы глубоко погрузиться в обсуждение поведения света в природе и моделирования этого поведения в Direct3D. Но мы не будем этого делать. Хоть свет и физическое явление, он не рассматривается в данной книге. Мы занимаемся созданием реалистично ведущих себя, а не красиво выглядящих объектов.
178 Глава 7 Замечание Цвет, освещение и материалы - это темы, тесно связанные с текстуриро- ванием. Чтобы игры выглядели профессионально сделанными, разработчики должны разбираться во всех этих темах. Хорошее введение в них - например, книга Mason McCuskey «Special Effects Game Programming with DirectX» (издательство Premier Press). Для наших целей нам достаточно знать, что DirectX обладает обширными возможностями по работе с освещением в ЗБ-сценах. Он поддерживает несколько видов света — как от точечных источников, так и рассеянного. Пока нас интересует только рассеянный свет. Цвет объекта, который мы видим на экране, когда объект отображается с помощью Direct3D, определяется цветом материала объекта и цветом падающего на объект света. Если на объект нанесена текстура, то оказывает влияние и ее цвет. Но пока не будем пытаться разобраться подробнее. Шарик, отображаемый в примере программы, будет синего цвета. Все, что нам нужно, чтобы шарик был синим и круглым. Используя рассеянный свет, мы можем этого добиться. Поэтому давайте внесем в платформу небольшие изменения, которые позволят нам использовать рассеянный свет. Сначала нужно изменить функцию InitD3D () в файле PMD3DApp.срр, чтобы она задействовала возможности моделирования освещения в DirectSD. Посмотрите на исходный код в файле с компакт-диска. Откройте файл PMD3DApp.cpp в папке Source\Chapter07\PointMass и найдите в нем функцию InitD3D (). В конце этой функции есть такая строка: TheApp.d3dDevice->SetRenderState(D3DRS_LIGHTING, theApp.enableD3DLighting); В предыдущих главах моделирование освещения отключалось, поэтому эта строка выглядела как theApp.d3dDevice->SetRenderState(D3DRS_LIGHTING,TRUE); Теперь нужно передать эту информацию платформе в функции ОпАр- pLoad(). Код этой функции приведен ниже в листинге 7.5. Листинг 7.5. Новая версия функции OnAppl_oad{) 1 bool OnAppLoad() 2 < 3 // Задаем параметры инициализации окна. 4 window_init_params windowParams; 5 windowParams.appWindowTitie = "Point Mass Test"; 6 windowParams.defaultX=l0 0; 7 windowParams.defaultY=100; 8 windowParams.defaultWidth = 400;
Динамика материальных точек 179 9 windowParams.defaultHeight = 400; 10 11 // Задаем параметры инициализации Direct3D. 12 d3d_initjparams d3dParams; 13 d3dParams.renderingDeviceClearFlags = D3DCLEAR_TARGET 14 | D3DCLEAR_ZBUFFER; 15 d3dParams.surfaceBackgroundColor = D3DCOLOR_XRGB{50,50,50); 16 d3dParams.enableAutoDepthStencil = true; 17 d3dParams.autoDepthStencilFormat = D3DFMT_D16; 18 d3dParams.enableD3DLighting = true; 19 20 // Этот вызов ДОЛЖЕН присутствовать в этой функции. 21 theApp.InitApp(windowParams,d3dParams); 22 23 return (true); 24 } He забудьте, что функция OnAppLoad () необходима платформе. В этом примере программы она находится в файле PointMassTest.cpp. Как и в главе 6 «Сетчатые модели и Х-файлы» программа передает параметры инициализации Windows и Direct3D с помощью структуры типа d3d_init_params. В нее добавлено несколько новых элементов, позволяющих задавать местоположение и размеры окна. Кроме того, один из новых элементов указывает, нужно ли включать или отключать моделирование освещения при запуске программы. Вот определение новой версии структуры: struct d3d_init_params { DWORD renderingDeviceClearFlags; D3DCOLOR surfaceBackgroundColor; bool enableAutoDepthStencil; D3DFORMAT autoDepthStencilFormat; bool enableD3DLighting; >; В строке 18 листинга 7.5 функция OnAppLoad () устанавливает в true элемент этой структуры enableD3DLightning. Затем структура передается функции InitApp(). Эта функция - элемент класса d3d_app. Данный класс определен в файле PMD3DApp. h, который тоже находится в папке Source\Chapter07\PointMass на компакт-диске. Код функции InitApp () приведен в листинге 7.6. Как видно из листинга 7.6, новая версия функции InitApp () просто копирует нужные данные из структуры в новые элементы класса d3d_app. Взгляните на листинг 7.7, в котором приведено новое определение этого класса.
180 Глава 7 Листинг 7.6. Новая версия функции InitAppO 1 inline bool d3d_app::initApp( 2 window_init_params windowParams, 3 d3d_init_params d3dParams) 4 < 5 // Задаем параметры инициализации окна 6 windowTi tle=windowParams.appWindowTitie; 7 defaultX = windowParams.defaultX; 8 defaultY = windowParams.defaultY; 9 defaultHeight = windowParams.defaultHeight; 10 defaultWidth = windowParams.defaultWidth; 11 12 // Задаем параметры инициализации Direct3D. 13 deviceClearFlags = d3dParams.renderingDeviceClearFlags; 14 backgroundColor = d3dParams.surfaceBackgroundColor; 15 enableAutoDepthStencil = d3dParams.enableAutoDepthStencil; 16 autoDepthStencilFormat = d3dParams.autoDepthStencilFormat; 17 enableD3DLighting = d3dParams.enableD3DLighting; 18 19 applnitialized=true; 20 return(applnitialized); 21 ) Листинг 7.7. Новое определение класса d3d_app 1 class d3d_app 2 { 3 private: 4 // Свойства приложения. 5 bool applnitialized; 6 7 // Свойства окна. 8 std::string windowTitle; 9 int defaultX, defaultY; 10 int defaultHeight,defaultWidth; 11 12 // Свойства Direct3D. 13 LPDIRECT3D9 direct3D; // Используется для 14 // создания D3DDevice 15 LPDIRECT3DDEVICE9 d3dDevice; // Наше устройство 16 // рендеринга 17 LPDIRECT3DVERTEXBUFFER9 vertexBuffer; // Буфер для 18 // хранения вертексов 19 DWORD deviceClearFlags; 20 D3DCOLOR backgroundColor; 21 bool enableAutoDepthStencil; 22 D3DFORMAT autoDepthStencilFormat;
Динамика материальных точек 181 23 bool enableD3DLighting; 24 25 public: 26 d3d_app(); 27 bool InitApp( 28 window_init_params windowParams, 29 d3d_init_params d3dParams); 30 31 LPDIRECT3DDEVICE9 D3DRenderingDevice(void); 32 33 LPDIRECT3DVERTEXBUFFER9 D3DVertexBuffer(void); 34 void D3DVertexBuffer( 35 LPDIRECT3DVERTEXBUFFER9 vertexBufferPointer); 36 37 DWORD RenderingDeviceClearFlags(void); 38 D3DCOLOR BackgroundSurfaceColor(void); 39 40 friend INT WINAPI AppMain( 41 HINSTANCE hlnst, 42 HINSTANCE, 43 LPSTR, 44 INT); 45 friend HRESULT InitD3D( 46 HWND hWnd); 47 friend VOID CleanupD3D(); 48 ]; Просматривая листинг 7.7, обратите особое внимание на строки 9, 10 и 23. В них определены элементы, хранящие новую информацию, которая передается через функцию InitAppO . Как я уже упоминал ранее, эта информация используется в функции InitD3D (). Теперь, включив моделирование освещения в Direct3D, можно приступить к моделированию движения шарика. Вернемся к обсуждению использования класса d3dj?oint_mass в физическом моделировании. ИНИЦИАЛИЗАЦИЯ ОБЪЕКТА КЛАССА D3D_POlNT_MASS Если вы просмотрите функцию GameInitialization() в файле Point- MassTest. срр, то увидите, что она инициализирует объект класса d3d_po- int_mass. Объявление этого объекта выглядит так: d3d_point_mass theObject; Оно расположено в начале файла. Взгляните на листинг 7.8, чтобы увидеть, как в функции Gamelniti- alization() инициализируется объект класса d3d_point_mass.
182 Глава 7 Листинг 7.8. Функция GamelnitializationQ 1 bool Gamelnitialization() 2 < 3 // Загружаем модель шарика. 4 theObject.LoadMesh("bowlball.x"); 5 6 // Задаем начальное местоположение шарика. 7 theObject.Location(vector_3d(-5.Of,0.0,0.0)); 8 9 // Задаем его массу. 10 theObject.Mass(lO); 11 12 // 13 // Настраиваем направленный рассеянный свет. 14 // 15 D3DLIGHT9 light; 16 ZeroMemory( Slight, sizeof(light) ); 17 light.Type = D3DLIGHT_DIRECTIONAL; 18 19 D3DXVECTOR3 vecDir; 20 vecDir = D3DXVECTOR3@.Of, -l.Of, l.Of); 21 D3DXVec3Normalize((D3DXVECTOR3*)Slight.Direction,SvecDir); 22 23 // Задаем цвет рассеянного света 24 light.Diffuse.r = l.Of; 25 light.Diffuse.g = l.Of; 26 light.Diffuse.b = l.Of; 27 light.Diffuse.a = l.Of; 28 theApp.D3DRenderingDevice()->SetLight( 0, Slight ); 29 theApp.D3DRenderingDevice()->LightEnable( 0, TRUE ); 30 theApp.D3DRenderingDevice()->SetRenderState( 31 D3DRS_DIFFUSEMATERIALSOURCE, 32 D3DMCS_MATERIAL); 33 34 return (true); 35 } Эта версия функции Gamelnitialization () выполняет три основные задачи. В строке 4 листинга 7.8 она загружает сетчатую модель объекта, представляемого материальной точкой. Это модель шара для боулинга, хранящаяся в файле bowlball .х. Этот файл хранится в папке программы Source\Chapter07\PointMass на компакт-диске. После этого функция Gamelnitialization (> задает свойства материальной точки. Она определяет начальное местоположение шарика - слева от окна программы. Поэтому вначале шарик в окне не виден. Кроме того, задается масса шарика. Она равна 10 кг B2 фунта) - довольно много для боулинг-шара.
Динамика материальных точек 183 И, наконец, функция Gamelnitialization () настраивает рассеянное освещение для Direct3D. В строке 15 объявлена переменная типа D3DLIGHT9. Ее содержимое обнуляется вызовом функции Windows Zero- Memory () . В строке 16 источник света делается направленным. В строках 19-21 для определения вектора, задающего направление света, используется переменная типа D3DXVECTOR3. В строках 24-27 задается белый цвет света. В строке 28 Direct3D отдается указание использовать созданный источник света, а в строке 29 - включить этот источник. В строках 30-32 совмещением цветов света и материала шарика получается цвет, который будет отображаться на экране. Ну что ж, все готово. У нас есть материальная точка с загруженной моделью. У нас есть источник освещения. Вероятно, можно было бы отпустить какую-нибудь кинематографическую шутку, но, к сожалению, я не могу вспомнить подходящую. Поэтому, пожалуйста, просто продолжайте читать. ОБНОВЛЕНИЕ МЕСТОПОЛОЖЕНИЯ ОБЪЕКТА КЛАССА D3D_POiNT_MASS В ходе выполнения программы нужно заново вычислять местоположение материальной точки при рендеринге каждого кадра анимации. Как и в предыдущих главах, это делается в функции UpdateFrame (). Чтобы увидеть ее исходный код, взгляните на листинг 7.9. Листинг 7.9. Функция UpdateFrameQ 1 bool UpdateFrame() 2 { 3 // Создаем матрицу отображения - как в предыдущих примерах. 4 D3DXVECTOR3 eyePoint@.Of,3.Of,-5.Of); 5 D3DXVECTOR3 lookatPoint@.О f,0.0 f,0.0 f) ; 6 D3DXVECTOR3 upDirection@.Of,1.Of,0.Of); 7 D3DXMATRIXA16 viewMatrix; 8 D3DXMatrixLookAtLH(SviewMatrix, SeyePoint, SlookatPoint, 9 fiupDirection); 10 theApp.D3DRenderingDevice()->SetTransform(D3DTS_VIEW, 11 SviewMatrix); 12 13 // Создаем матрицу проецирования - как в предыдущих примерах. 14 D3DXMATRIXA16 projectionMatrix; 15 D3DXMatrixPerspectiveFovLH(SprojectionMatrix, 16 D3DX_Pl/4,1.0f,1.0f,100.Of) ; 17 theApp.D3DRenderingDevice() 18 ->SetTransform(D3DTS_PROJECTION, SprojectionMatrix); 19 20 // 21 // В течение одного интервала времени прикладываем к шарику 22 // силу.
184 Глава 7 23 // Эта инициализация выполняется только один раз. 24 static bool forceApplied = false; 25 26 // Если сила еще не прикладывалась... 27 if (!forceApplied) 28 { 29 // Прикладываем силу. 30 theObject.Force(vector_3dB.Of,0.0,0.0)) ; 31 forceApplied = true; 32 } 33 // Иначе сила уже прикладывалась... 34 else 35 { 36 // Присваиваем ей нулевую величину. 37 theObject.Force(vector_3d@.0,0.0,0.0)); 38 ) 39 40 /* Задайте параметру значение между 0 и 1 для более 41 плавной анимации. */ 42 theObject.Update(l); 43 44 return (true); 45 ) Функция DpdateFrame () начинается так же, как и аналогичные функции в предыдущих главах. Сначала она подготавливает матрицы, требующиеся Direct3D. Когда эти матрицы готовы, она один раз прикладывает к шарику силу, чтобы он начал двигаться. Чтобы сделать это, функция использует статическую переменную. Возможно, вы знаете, что в C++ статические переменные в функциях инициализируются один раз - при первом вызове этих функций. После этого инициализация никогда не выполняется во второй раз. Использование статической переменной позволит нашей функции определить, прикладывалась ли уже к шарику сила. Если нет, то переменная forceApplied будет установлена в значение false. Поэтому оператор if, начинающийся в строке 27, задаст силу, действующую на шарик (строка 30). Кроме того, он установит переменную forceApplied в значение true. Замечание Обратите внимание, что сила действует в направлении увеличения значений по оси х. Поэтому шарик будет двигаться по экрану слева направо. Изначально он находится слева от области, видимой в окне программы. Когда программа запустится, он переместится по окну программы и исчезнет за его правым краем. После этого ничего нового в окне не отобразится, поэтому его можно закрыть.
Динамика материальных точек 185 При следующем вызове функции UpdateFrame () переменная f ог- ceApplied сохранит свое значение - true, поэтому выполнится блок выражений в операторе else. Этот блок выражений уменьшит силу до нуля, поэтому шарик получит толчок только при первом выполнении функции UpdateFrame (). После этого на него не будут действовать никакие силы. Шарик будет двигаться бесконечно, пока выполняется программа. Приятно видеть, что наша имитация работает в соответствии с законами Ньютона. Это свидетельствует о том, что мы правильно составили программу. Последнее, что делает функция UpdateFrame () - вызывает для объекта-шарика метод d3d_point_mass: : Update (). Этот метод пересчитывает местоположение шарика, исходя из действующих на шарик сил (если таковые есть), скорости шарика и его ускорения. Если хотите посмотреть еще раз на код метода d3d_j>oint_mass: : Update (), он приведен в листинге 7.3. РЕНДЕРИНГ ОБЪЕКТА КЛАССА D3D_POINT_MASS Рендеринг объекта класса d3d_point_mass - действительно сложная процедура. Ужасно сложная. Но если вы еще не испугались, то можете посмотреть на код, выполняющий этот рендеринг. Он приведен в листинге 7.10. Листинг 7.10. Рендеринг объекта класса d3d_point_mass 1 bool RenderFrame() 2 { 3 theObj ect.Render{) ; 4 return (true); 5 } Впечатляет? Хм. Ладно, я пошутил. Если вы недавно занимаетесь компьютерной графикой, то вряд ли сможете представить себе, как это здорово — иметь возможность выполнить рендеринг сложного объекта вроде шарика с помощью такой короткой функции. Я начинал работать с компьютерной графикой больше 20 лет назад. Тогда, чтобы вывести что-нибудь на экран, мне приходилось писать собственный модуль рендеринга. Этот модуль должен был быть написан на ассемблере. Если вы не знаете, что это значит, можете считать, что вам повезло. Да, в те годы жизнь иногда была чертовски сложной. В любом случае, из листинга 7.10 видно, что функция RenderFrame () просто вызывает метод Render () класса d3d_j?oint_mass для объекта theObject. Этот метод, в свою очередь, вызывает метод класса d3d_mesh, чтобы применить глобальную матрицу, созданную методом Update () класса d3d_point_mass. Кроме того, вызывается метод d3d_mesh: : Render (), чтобы выполнить рендеринг сетчатой модели. Поскольку эти методы выполняют все нужные действия, на долю функции RenderFrame () остается немногое. Другими словами, если мы используем объект класса
186 Глава 7 d3d_point_mass, нам нужно настроить его, приложить к нему силу и позволить ему двигаться. Больше ничего делать не нужно. Неплохо, правда? Материальные точки в играх Часто ли используются материальные точки в играх? Почти постоянно. И чем дальше, тем чаще они используются. С увеличением вычислительной мощи компьютеров важность материальных точек в играх будет возрастать. Позвольте привести пример. Предположим, что в игре персонаж может стрелять в стену. В таких играх не обязательно использовать материальные точки. Когда пуля попадает в стену, игра применяет к стене текстуру, отображающую щербину в стене и обесцвечивание от пороха. Но на самом деле стена не повреждается. Это неплохо, но в последнее время игры становятся более реалистичными. В некоторых играх используются системы маленьких частиц, чтобы изобразить фрагменты стены, отлетающие от нее при попадании. Эти фрагменты исчезают по прошествии некоторого времени. Однако на самом деле стена все равно не повреждается. Если у нас есть достаточные вычислительные ресурсы, мы можем смоделировать появление дыр в стене при попаданиях. Программе придется отслеживать, с какой силой пуля врезается в стену. Если игрок стреляет в стену издалека, пуля не должна пробить стену. Если стрельба ведется в упор, то в стене должны появляться дыры. Чтобы моделировать такое поведение, нужно использовать материальные точки. Сложные объекты часто представляются в виде наборов материальных точек. Если мы создадим объект, состоящий из тяжелых частей, соединенных более легкими, его можно будет представить в виде набора материальных точек. Если это сделать, то объект можно красиво разрушить. Например, при взрыве могут разлететься в разные стороны части объекта. Остается добавить зрелищные эффекты взрыва, и все будет выглядеть очень реалистично. Итоги Вот и все о кинематике материальных точек. Мы далеко продвинулись! Начав с основных понятий о материальных точках и скоростях, мы создали систему, позволяющую моделировать материальные точки, и применили ее. Мы еще не затрагивали силу тяжести, отскок объектов при столкновениях, трение и сцепление, но скоро мы это сделаем. Пока у нас есть весьма реалистичная модель шарика, и этого достаточно.
Глава 8 Столкновения материальных точек В созданной нами в предыдущей главе модели материальные точки могут двигаться под воздействием приложенных к ним сил. Мы можем добавлять в модель все новые силы, делая ее все более реалистичной (и сложной), но нам по-прежнему будет не хватать одной важной вещи: материальные точки не взаимодействуют между собой. В этой главе мы займемся устранением данного недостатка. Но прежде чем мы займемся моделированием столкновений, нужно уяснить: это моделирование на самом деле состоит из двух отдельных проблем. Первая - обнаружение столкновений. Чтобы игра могла отреагировать на столкновение, она должна знать, что столкновение произошло. Эта проблема может показаться простой, но на самом деле это совсем не так. Обнаружение столкновений - сложная задача. В любом случае - обнаружение столкновений есть задача программирования, а не физическая задача, поэтому она не рассматривается в этой книге подробно. Мы только кратко рассмотрим основные методы обнаружения столкновений и перейдем ко второй проблеме, связанной со столкновениями: реакции на столкновения. Реакция на столкновения - это физическая задача. Под реакцией на столкновения подразумевается поведение сталкивающихся объектов в момент столкновения и после него. Это поведение определяется физикой. После обзора методов обнаружения столкновений мы займемся моделированием реакции на столкновения для материальных точек. Обнаружение столкновений Научные центры полны дипломированных специалистов, пытающихся создать новые, более быстрые и эффективные алгоритмы обнаружения столкновений, поэтому на эту тему есть горы литературы, в которых вы можете (при желании) копаться всю оставшуюся жизнь. В компьютерных играх самые лучшие решения - обычно самые простые, поэтому мы рассмотрим только самые основные методы обнаружения столкновений.
188 Глава 8 Ограничивающие сферы Это самый простой метод обнаружения столкновений. Поместите ваш объект в центр сферы, причем радиус сферы должен быть минимальным, при котором объект полностью находится внутри сферы, как на рисунке 8.1. Если другой объект попадает внутрь этой сферы, можно считать, что произошло столкновение. Рис. 8.1. Объект с ограничивающей сферой Например, этот метод можно применить для поиска столкновений объектов с плоскостью земли. Вот последовательность действий: 1. Проверим, находится ли объект над плоскостью. Если да, переходим к шагу 2. 2. Сравним расстояние от материальной точки до поверхности и радиус ограничивающей сферы этой материальной точки. Если радиус больше этого расстояния, произошло столкновение, поэтому переходим к шагу 3. В противном случае пропускаем шаг 3 и повторяем шаги 1 и 2 для следующего объекта в сцене. 3. Объект столкнулся с землей. Просчитать реакцию на столкновение. Этот метод работает не только для земли, но и для любой другой плоской поверхности. На рисунке 8.2 показано, как можно применить его для обнаружения столкновений со стеной. Рассматриваемые объекты не обязательно должны быть шариками. Они могут быть любой формы, просто их нужно поместить в ограничивающие сферы. Вариацию этого алгоритма можно применить в игре для обнаружения столкновений материальных точек между собой, как показано на рисунке 8.3. Чтобы определить, произошло ли столкновение между объектами, нужно знать радиусы ограничивающих сфер и векторы местоположений этих объектов. Затем нужно сравнить сумму радиусов сфер и расстояние
Столкновения материальных точек 189 между объектами, как ка рисунке 8.4. Если расстояние между объектами меньше суммы радиусов их ограничивающих сфер, значит, произошло столкновение. радиус Рис. 8.2. Обнаружение столкновений шарика со стеной Рис. 8.3. Столкновение двух объектов, представленных ограничивающими сферами радиус объекта 1 радиус объекта 2 радиус объекта 1 радиус объекта 2 расстояние расстояние столкновения нет столкновение Рис. 8.4. Сравнение расстояния между объектами и суммы их радиусов Используя векторы, требуемые вычисления провести несложно. Предположим, что мы пишем программу игры в бильярд, и нам нужна функция, определяющая, произошло ли столкновение двух шаров. Если у вас
190 Глава 8 есть координаты центров шаров, представленные в виде векторов, то расстояние между ними можно найти, вычтя один вектор из другого. Это демонстрирует рисунок 8.5. Вычтя вектор pj из вектора р2 с рисунка 8.5, мы получим вектор расстояния между двумя центрами масс. Теперь программе осталось только найти длину вектора расстояния и сравнить ее с суммой радиусов ограничивающих сфер. Если эта длина меньше суммы радиусов, значит, произошло столкновение. Если нет, столкновения нет. Pi Р2 Pi I/ Рис. 8.5. Нахождение вектора расстояния к между двумя объектами Если описанные выше действия не совсем понятны, посмотрите на следующий фрагмент кода. Он использует два объекта класса d3d_po- int_mass — balll и ball2 - и вычисляет расстояние между ними как длину вектора расстояния. После этого он сравнивает найденное расстояние и сумму радиусов. // Вычисляем разницу векторов местоположения сфер. vector_3d distance = balll.Location() - Ьа112.location(); // Находим расстояние между центрами сфер (длину вектора расстояния) scalar magnitude = distance.Norm(); // Вычисляем расстояние, при котором произойдет столкновение. scalar minDistance = (balll.Radius() + ball2.Radius()); if (magnitude < minDistance) { // Произошло столкновение. Нужно смоделировать реакцию на него. } Этот алгоритм прекрасно работает, но у него есть очень существенный недостаток: он очень медленный! Вспомните - нужно проверять, произошло ли столкновение между каждой парой частиц в сцене. И при каждой проверке нужно вычислять квадратный корень. Это очень медленная операция - она выполняется почти в 70 раз дольше, чем операция умножения двух чисел с плавающей запятой. Вспомните код метода vec- tor_3d: : Norm (), вычисляющего норму вектора:
Столкновения материальных точек 191 inline scalar vector_3d::Norm(void) { return(sqrtf(x*x + y*y + z*z)) ; } Ускорить работу этого алгоритма можно несколькими способами. Можно, например, использовать быстрые способы приближенного вычисления квадратных корней. Но внимательные читатели, вероятно, предложат еще лучший способ: вообще не вычислять квадратный корень! Если у нас есть два положительных числа х и у, то, если х больше у, то и х2 будет больше у2, правда? Так почему бы нам ни сравнивать квадрат расстояния с квадратом суммы радиусов вместо расстояния с суммой радиусов? Вот код, в котором реализована эта идея: // Вычисляем разницу векторов местоположения сфер. vector_3d distance = balll.Location() - Ьа112.location(); // Находим расстояние между центрами сфер (длину вектора // расстояния). scalar magnitude = distance.NormSquared(); // Вычисляем минимальное расстояние, при котором столкновение // не произойдет. scalar minDistance = (balll.Radius() + ba!12.Radius()); // Возводим расстояние в квадрат. minDistance *= minDistance; if (magnitude < minDistance) { // Произошло столкновение. Нужно смоделировать реакцию на него. } В этом фрагменте используется метод vector_3d:: NormSquared (), похожий на vector_3d: :Norm(), но не вычисляющий квадратные корни: inline scalar vector_3d: .-Norm(void) { return((x*x + y*y + z*z); ) В новом варианте алгоритма есть дополнительная операция умножения чисел с плавающей запятой — она нужна для возведения суммы радиусов в квадрат. Однако эта операция выполняется молниеносно по сравнению с вычислением квадратного корня. Использование ограничивающих сфер - часто лучший способ обнаружения столкновений. Этот способ прост, быстро работает и позволяет получать прекрасные результаты для многих задач. Если вы хотите использовать более изощренные способы обнаружения столкновений, все же попробуйте сначала использовать способ с ограничивающими сферами. Возможно, его будет достаточно, и не придется дополнительно нагружать процессор, используя более изощренные способы.
192 Глава 8 Ограничивающие цилиндры Вместо сфер можно ограничивать объекты некоторыми более сложными фигурами. Ограничивающие цилиндры очень удобно применять в играх, когда большинство объектов не изменяет свою ориентацию относительно определенной поверхности. Хорошие примеры таких игр - Doom и похожие на него стрелялки, в которых большинство персонажей не пригибается, даже оказавшись под ураганным огнем. Все персонажи сохраняют постоянную ориентацию относительно пола. На рисунке 8.6 показано применение ограничивающего цилиндра. Чтобы обнаружить столкновения, нужно проверять пересечения верхнего и нижнего срезов цилиндра, а не только его боковой поверхности. Первый шаг к реализации такого подхода - добавление элементов данных, нужных для хранения размеров цилиндра, в класс d3d_point_mass. На рисунке 8.7 показаны эти размеры относительно местоположения объекта. Рис. 8.6. Объект с ограничивающим цилиндром Радиус Высота Рис. 8.7. Размеры ограничивающего цилиндра Чтобы выяснить, столкнулись ли два цилиндра, нужно выполнить два действия. Предположим для начала, что цилиндры всегда ориентированы так, как показано на рисунке 8.6 и 8.7. Это позволит нам превратить задачу из трехмерной практически в двумерную. Радиус ограничивающих цилиндров будет всегда лежать в плоскости xz. Поэтому первый шаг - рассматривать цилиндры как окружности в плоскости xz. Найдем
Столкновения материальных точек 193 расстояние между центрами этих окружностей. Если это расстояние больше суммы радиусов окружностей, значит, столкновения нет, и следующий шаг выполнять нет необходимости. Если расстояние меньше этой суммы, то столкновение возможно, и нужно выяснить, произошло ли оно. Для этого служит следующий шаг. Следующий шаг - выяснить, есть ли пересечение цилиндров по высоте, как показано на рисунке 8.8. Если верхний край одного из цилиндров находится на высоте между нижним и верхним краями другого цилиндра, то столкновение произошло, и программа должна на него отреагировать. Если нет, то столкновения нет, несмотря на то, что радиусы цилиндров перекрываются. Это может происходить, например, если персонажи находятся друг над другом на разных этажах здания. к"' Рис. 8.8. Проверка пересечения цилиндров в вертикальной плоскости Ограничивающие блоки Теперь, познакомившись с ограничивающими сферами и ограничивающими цилиндрами, вы, вероятно, не удивитесь, узнав, что можно использовать для обнаружения столкновений и прямоугольные блоки, вроде показанного на рисунке 8.9. Их можно использовать как в двумерных, так и в трехмерных системах координат. Рис. 8.9. Ограничивающий блок Используя ограничивающую сферу или ограничивающий цилиндр, мы предполагаем, что у ограничиваемого объекта более-менее подходящая для них форма. Иногда это удобно, но иногда может вызывать сложности.
194 Глава 8 Взгляните на рисунок 8.10, на котором изображен плоский многоугольник. Хотя ограничение окружностью работает, оно перекрывает большую площадь, не относящуюся к многоугольнику. В этом случае лучше применить ограничение прямоугольником. Рис. 8.10. Выбор ограничивающей фигуры, оптимальной для объекта Выяснить, произошло ли столкновение двух прямоугольных блоков, можно следующим образом. Выберите вертекс одного из блоков. Затем проверьте, находится ли этот вертекс внутри другого блока. Если да, значит, произошло столкновение, как показано на рисунке 8.11. Если нет, то возьмите следующий вертекс и выполните для него такую же проверку. Всего у прямоугольного блока восемь вертексов. Обратите внимание, что нужно проверить все вертексы обоих блоков. Чтобы убедиться в этом, посмотрите на рисунок 8.11. Если проверить только вертексы левого блока, мы не обнаружим столкновения. \ / Рис. 8.11. Два блока столкнулись, Ч / если вертекс одного из них находится _J N/ внутри другого у2, z2) Рис. 8.12. Блок, ребра которого параллельны осям декартовой системы координат, можно описать двумя вертексами 1x2. 12 (Х1,у1, 21)
Столкновения материальных точек 195 Если ребра прямоугольного блока параллельны осям координат, как на рисунке 8.12, то его можно описать двумя вертексами. Я буду называть эти вертексы (xl, yl, zl) и (х2, у2, z2). Проверить, попадает ли в этот блок какой-то вертекс другого блока, весьма просто. Это делается почти так же, как проверка пересечения цилиндров в вертикальной плоскости - только для каждого из трех измерений: if (х >= xl && х <= х2) // Попадает по оси х if (у >= yl && у <= у2) // Попадает по оси у if (z >= zl && z <= z2) // Попадает по оси z /* вертекс внутри блока */ Обнаружение столкновений с помощью ограничивающих блоков сильнее нагружает процессор, чем их обнаружение с помощью ограничивающих сфер или цилиндров, но оно часто дает прекрасные результаты. Трех только что рассмотренных методов вам хватит для решения практически любых задач. Замечание Ограничивающий блок, грани которого параллельны осям координат, называется ограничивающим блоком, выровненным по осям (axis-aligned bounding box - ААВВ). Эта аббревиатура часто встречается в литературе по программированию игр. Оптимизация с помощью пространственного разделения Количество возможных столкновений между разными объектами Nc в кадре можно вычислить по следующей формуле: Nc = n!/B!(n - 2)!) Здесь п - количество объектов. Для больших значений п величина Nc будет приблизительно равна п2/2. Факториалы Обозначение п! читается как «факториал от п». вольно сложно объяснить словами, поэтому я числения факториала и несколько примеров: Факториалом нуля считается факториалов для других чисег 3! = 3x2x1=6 5! = 5x4x3x2x1 = 120 6!/4! = Fх5х4хЗх2х п!=Пк к=1 единица @! = 1) : 1) / D х 3 х 2 х Что такое факториал, приведу формулу для Вот 1) = до- вы- несколько примеров 6 х 5 = 30
196 Глава 8 В таблице 8.1 приведены количества возможных столкновений для нескольких значений п. Из этой таблицы можно увидеть, что это количество быстро растет с увеличением п. Учтите - 10 000 объектов не слишком много для игры, в которой просчитывается движение каждой пули. А ведь при этом нужно проверять почти 50 000 000 возможных столкновений! Так что алгоритмы обнаружения столкновений предоставляют множество возможностей для совершенствования. Игрок может взаимодействовать только с объектами, находящимися в затемненных ячейках ,. _ f шнгллнпимшИ^Я II, «Г ^^l Рис. 8.13. Ускорение обнаружения столкновений с помощью пространственного разделения Вероятно, вы уже поняли - как бы мы ни ускоряли просчет отдельных столкновений, это нам не поможет. Единственный выход - уменьшение количества возможных столкновений, которые нужно просчитывать. Один из способов уменьшения этого количества - пространственное разделение (spatial partitioning). Этот способ основан на разделении пространства на ячейки, как показано на рисунке 8.13. Нужно проверять только столкновения между частицами в смежных ячейках или в одной и той же ячейке.
Столкновения материальных точек 197 Таблица 8.1. Количество возможных столкновений Количество объектов Количество возможных столкновений 2 1 4 6 10 45 20 190 100 4950 1 000 499 500 10 000 49 995 000 Подсказка Пространственное разделение можно использовать и при прорисовке графики. Зачем просчитывать и прорисовывать объекты, которые не видны? Предположим, что мы поделим мир на ячейки - 10 X 10 X 10. В общей сложности будет 1000 ячеек. Если у нас 10 000 объектов, то в каждой ячейке будет в среднем 10 объектов. Если взаимодействуют только объекты в блоке 3x3x3 вокруг игрока, то нужно проверить на столкновения всего лишь 270 объектов - это 270! / B! х 268!) или 36 315 возможных столкновений. Это куда лучше, чем почти 50 миллионов! Реакция на столкновения А что же происходит, когда объекты все-таки сталкиваются? Массовое замешательство — вот что происходит. Объекты деформируются. В них появляются трещины. Во все стороны разлетаются обломки. Раздается грохот, и летят искры. Объекты разогреваются, и появляется возмущение воздуха. Можем ли мы все это смоделировать? Увы, вряд ли. Вместо этого мы поступим так. Рассмотрим одномерное столкновение двух тел, как на рисунке 8.14. Два объекта с массами nij и т2 летят навстречу друг другу со скоростями Vj и v2, происходит столкновение, и объекты разлетаются в разные стороны со скоростями ух' и v2'. Вопрос заключается в следующем: зная массы и начальные скорости тел, можем ли мы найти скорости, с которыми они будут разлетаться?
198 Глава 8 гги О V1' пгJ v2 о о о V21 Рис. 8.14. Столкновение двух объектов Пока мы не будем обращать внимание на само столкновение (то есть на все вещи, которые происходят в ходе соударения объектов), и попытаемся просто найти связь между начальными скоростями объектов и их результирующими скоростями. Закон сохранения импульса Второй закон Ньютона можно записать в форме, отличной от той, которую мы использовали раньше. Запишем его так: F = dp/dt. Здесь F - сила, ар- импульс. Что такое импульс? В книгах по физике его обычно определяют как произведение массы тела на скорость его движения. Один мой знакомый профессор однажды предложил считать его инерцией тела в движении. По-моему, такое определение — лучшее из всех, которые я слышал. р = mv Представьте себе систему частиц, у каждой из которых есть свой импульс. Эта система находится в большой области пространства. Частицы перемещаются относительно других частиц, взаимодействуют с ними, но мы не прикладываем к этим частицам никаких внешних сил. В таком случае, согласно второму закону Ньютона: dp/dt = О
Столкновения материальных точек 199 Единственное решение такого уравнения - неизменный импульс. Оно показывает нам, что суммарный импульс системы такого типа не изменяется со временем. Это важный принцип. Говоря другими словами, если для системы справедливо уравнение Ар/At = О, то Ар = О Это потому, что деление на 0 приводит к неопределенному результату, следовательно, At не может быть равно 0, поэтому Ар должно быть равно 0. Следовательно, р' = р + Ар р'= р Это математическая форма записи закона сохранения импульса. С формальной точки зрения этот закон действует так: в системе, на которую не действуют внешние силы, суммарный импульс остается постоянным. Мы можем применить этот закон к задаче о столкновении. Поскольку импульс остается неизменным, то сумма импульсов тел до столкновения должна быть равна сумме их импульсов после столкновения: Pi + Р2 = Pi + Р2 Здесь pj - импульс первого тела до столкновения, р^' - его же импульс после столкновения, а р2 и р2' - импульсы второго тела до столкновения и после. Будем считать, что при столкновении от объектов не откалываются обломки, которые могут унести с собой часть импульса. Подставляя формулу р = mv для каждого из моментов, получаем: mj^Vj + m2v2 = nijVj' + m2v2' Поскольку мы пока рассматриваем одномерные столкновения, можно переписать это равенство в виде m-jv^ + m2V2 = rnjVj/ + m2v2* Это прекрасный результат. Мы получили отношение, связывающее скорости тел до столкновения и их скорости после столкновения, не учитывающее характер самого столкновения. Но этого соотношения недостаточно, чтобы найти нужные нам скорости Vj/ и v2'. Для этого нам необходимо еще одно уравнение.
200 Глава 8 Энергия Прежде чем мы продолжим изучение столкновений, нужно разобраться с концепциями энергии и работы. Если продолжать толкать с постоянной силой движущийся объект в направлении его движения, как на рисунке 8.15, этот объект будет двигаться все быстрее и быстрее. Но на постоянное подталкивание объекта требуются усилия. Эти усилия называются работой (work), и работу можно вычислить как скалярное произведение силы на перемещение: W = F • х х Рис. 8.15. Приложение силы к движущемуся объекту Предположим, что мы разгоняем с постоянным ускорением объект, который вначале неподвижно стоит в точке х = 0, до скорости v за время t. В этом случае мы можем применить одно из следующих уравнений: х = A/2) at2 v = at Кроме того, у нас есть второй закон Ньютона: F = та Объединяя эти формулы, мы можем найти работу, которую нужно совершить, чтобы разогнать объект массы m до скорости v: W = Г • х = ma • х = ma • A/2) at2 = A/2) m(a • a)t2 = A/2) m(at • at) = A/2) m(v • v) = A/2) mv2
Столкновения материальных точек 201 Большая часть этих преобразований — простые подстановки. Кроме того, в них использован тот факт, что скалярное произведение вектора на самого себя дает квадрат нормы этого вектора. Предупреждение Не забывайте, что норма вектора может обозначаться двумя способами: |v| или v. Полученное нами выражение покажется знакомым тем из вас, кто изучал физику. Результат наших преобразований - формула для нахождения кинетической энергии. Это энергия, накопленная движущимся телом. Мы будем обозначать ее буквой К: К= A/2) mv2 Работу можно воспринимать как изменение энергии системы под воздействием внешних сил. Если на систему не действуют никакие силы, то никакая работа не выполняется, и энергия системы остается неизменной. Этот принцип называется законом сохранения энергии. Упругие столкновения А теперь вернемся к изучению столкновений. Если помните, с помощью закона сохранения импульса мы получили такое соотношение: mjVj + m2v2 = nijVj' + m2v2' Здесь nij - масса первого сталкивающегося тела, т2 - второго, vt и v2 - скорости тел до столкновения, a Vj' и v2' - их скорости после столкновения. Закон сохранения энергии говорит, что суммарная энергия тел до столкновения будет равна их суммарной энергии после столкновения. Если предположить, что вся энергия тел до столкновения сохранится и после столкновения, то можно написать: ^1 + К2 = Kj' + К2' Здесь Kj и К2 - кинетические энергии тел до столкновения, аК['и К2' - их кинетические энергии после столкновения. Если подставить в это соотношение формулы для вычисления кинетических энергий, мы получим: (l/2)mlVl2 + (l/2)m2v22 = (l/2)mlVl'2 + (l/2)m2v2'2 Замечание Это уравнение работает как для одномерных, так и для двух- и трехмерных столкновений.
202 Глава 8 Теперь у нас есть два уравнения и две переменные, и мы можем найти скорости тел после столкновения. Преобразования, которые для этого нужно выполнить, весьма тривиальны, но чересчур объемны, чтобы приводить их здесь. Поэтому ограничимся результатами: vl = _ (rtij - n^v^ 2m2v2 ml+m2 (m2 - m1)v2+ ImjVj n^+rn^ Неупругие столкновения Предположим, что мы моделируем столкновение двух комков глины массой mj и т2, как на рисунке 8.16. При столкновении они слипнутся вместе, образовав один комок с массой mj + m2. mi ГП2 m-j + ГП2 Рис. 8.16. Неупругие столкновения В данном случае мы можем, используя закон сохранения импульса, записать: mjVj + m2v2 = (m-L + m2)v' Здесь v' есть скорость получающегося комка. Преобразовав это выражение, мы получим: v = ш^! + т^Дш! + т2) Вот так! Для неупругого столкновения можно найти скорость образующегося объекта, вообще не используя понятие энергии.
Столкновения материальных точек 203 Замечание Неупругие столкновения встречаются в играх довольно часто. Если вы всаживаете пулю в череп противника, и пуля не отскакивает рикошетом, то массы пули и противника объединяются, и образовавшееся тело отлетает в каком-то направлении. Вас может смутить один момент. Если при упругом столкновении мы получили результат, исходя из законов сохранения импульса и энергии, то почему же мы получили другой результат при неупругом столкновении? Причина в том, что при неупругом столкновении не вся энергия уходит в движение объектов после столкновения. Когда слипаются комки глины, они деформируются, и в них генерируется тепло. Энергия сохраняется, просто мы не отслеживаем ее преобразования. Коэффициент восстановления Посмотрим еще раз на уравнения законов сохранения импульса и энергии для упругого столкновения: miVj + m2v2 = nijV]/ + nti2V2' A/2I11^ + (l/2)m2v22 = (l/2)mlVl'2 + (l/2)m2v2'2 Давайте перепишем эти уравнения немного по-другому: ml(vl - vl') = -m2(v2 - v2') ml(vl ~ vl') (vl + vl') = ~m2(v2 ~ V) (v2 + v2') Если разделить первое уравнение на второе и выполнить некоторые преобразования, мы получим: - (Vl - V2> _ = 1 (для упругого столкновения) Vl~ V2 Проще говоря, разность скоростей после столкновения равна разности скоростей до столкновения - меняется только направление движения. А как будет выглядеть аналогичное соотношение для неупругого столкновения? После столкновения две частицы превращаются в одну, поэтому Vj' = v2'. Соответственно, эта величина равна 0: ! — = 0 (для неупругого столкновения)
204 Глава 8 В реальном мире нет совершенно упругих или совершенно неупругих столкновений. В большей части столкновений какая-то часть энергии уходит на деформацию сталкивающихся тел и другие эффекты, но объекты не остаются соединенными. Чтобы описать такие столкновения, нам понадобится коэффициент восстановления (coefficient of restitution), который мы обозначим е. с_ -(у;-у2) vl- v2 Для полностью неупругого столкновения е = 0; для полностью упругого е = 1. Обычно мы будем задавать для е какие-то значения между 0 и 1. Например, для биллиардных шаров стоит выбрать значение чуть меньше единицы - они почти не деформируются и не разогреваются при ударах. Для тряпичных мячей значение должно быть заметно меньше - вероятно, около 0.2. Замечание Можно получить занятные результаты, задав коэффициент восстановления больше единицы. При этом в системе энергия будет не сохраняться, а на- растать в результате столкновений. УРАВНЕНИЯ ДЛЯ СТОЛКНОВЕНИЙ Используя коэффициент восстановления и уравнение закона сохранения импульса, можно получить универсальные уравнения, которые мы и будем использовать для расчета столкновений в нашей физической модели. Вот исходные уравнения: с_ -(у|-у2) vl- V2 mjV-L + m2v2 = niiV]/ + rn2V2' Преобразовав эти уравнения и выполнив подстановки, мы получим: (т1 - em2)Vj+ A + e)m2v2 nij+irij _ (m2 - em1)v2+ A + e)mjVj v Именно эти формулы нам и были нужны. По ним можно рассчитать скорости тел после любых линейных столкновений. Попробуйте подставить в них е = 0ие = 1и убедитесь, что получатся уравнения для полностью неупругих и полностью упругих столкновений.
Столкновения материальных точек 205 Столкновения материальных точек в двумерных и трехмерных системах координат Этот раздел будет коротким. Почему? Потому что в общем случае решение задачи столкновения тел в двух и трех измерениях будет слишком сложным. Обычно у нас будет недостаточно информации для нахождения решения, за исключением случаев полностью неупругих столкновений. При упругих столкновениях нам нужна дополнительная информация, скажем, законы взаимодействия между телами (например, закон тяготения Ньютона) или данные о форме тел. Столкновения сфер Поскольку просчитывать столкновения материальных точек слишком сложно, мы обратим наше внимание на сферы. Предположим, что у нас есть две массивные однородные сферы, которые сталкиваются без трения, как на рисунке 8.17. Их скорости до столкновения равны vj и v2, a скорости после столкновения равны Vj' и v2'. Обратите внимание на то, что точка контакта должна находиться на прямой, соединяющей центры сфер. Эта линия пересекает поверхности сфер под прямым углом. Рис. 8.17. Столкновение двух сфер Предупреждение Замечание об отсутствии трения существенно. Трение между поверхностями приведет к возникновению вращающего момента. Если вы не знаете, что такое вращающий момент, то узнаете это в главе 9 «Динамика твердых тел». Поскольку это единственная точка соприкосновения сфер, то прямая, соединяющая центры сфер, будет линией взаимодействия. Проще говоря, мы можем рассматривать проблему столкновения как одномерную, происходящую на этой прямой. Причина этого — в отсутствии изменений составляющей импульса, перпендикулярной этой прямой.
206 Глава 8 Рис. 8.18. Чтобы преобразовать данную проблему в проблему линейного столкновения, спроецируем векторы скоростей тел до столкновения на линию взаимодействия Чтобы найти эквивалентную этому столкновению одномерную проблему, нам достаточно спроецировать начальные скорости сфер на линию взаимодействия, как показано на рисунке 8.18. Используя проекции скоростей, мы можем решить проблему с помощью уравнений, которые мы вывели раньше для линейных столкновений. Найти единичный вектор для линии взаимодействия можно, нормализуя вектор расстояния между центрами двух сфер. Вектор расстояния можно найти вычитанием векторов местоположения сфер, как показано на рисунке 8.19. Рис. 8.19. Вектор расстояния между сферами есть разность векторов местоположения сфер Задав единичный вектор, который мы обозначим и, мы можем выразить проекцию вектора Vj как vj • п, а проекцию вектора v2 как v2 • п. Эти проекции можно подставить в уравнения для линейных столкновений: V '2р- _ (mj- em2)vlp+(l+e)m2v 2р mj+m2 (m2- emj)v2 +(l+e)mjv 'ip mj+ m2 Здесь vlp" и v2p' - это проекции скоростей сфер после столкновения на линию взаимодействия, как показано на рисунке 8.20. Из этих проекций можно получить скорости сфер после столкновения - нужно вычесть старый п компонент и прибавить новый.
Столкновения материальных точек 207 Нахождение скоростей после столкновения возвращает нас в трехмерное пространство - именно то, что нужно в ЗБ-играх. Рис. 8.20. Проекции векторов скоростей до и после столкновения vrvi+(vip-viP)n v2=v2+(v2p- v2p)n Реализация После всех этих рассуждений реализовать столкновения в коде программы будет просто. Мы будем реализовывать столкновение сфер, поэтому используем ограничивающие сферы в качестве метода обнаружения столкновения. Все остальное будет взято непосредственно из уравнений и рассуждений, которые приведены выше в этой главе. Первое, что нужно для реализации обнаружения столкновений и реагирования на них, - обновить класс d3d_point_mass. Затем нужно будет подготовить имитацию. Новые аспекты имитации потребуют обновить функцию UpdateFrame (). В каждом новом кадре программа должна проверять, не произошли ли столкновения, и, если произошли, то реагировать на них. Наконец, нужно выполнять рендеринг кадров. Посмотрим, как выполняются все эти задачи. Замечание Исходный код примера программы из этой главы есть на компакт-диске, поставляемом с книгой. Он находится в папке Source\Chapter08\Part- icleBounce. Если вы хотите просто посмотреть на программу в работе, ее исполняемый файл находится в папке Source\Chapter08\Bin. ОБНОВЛЕНИЕ КЛАССА D3D_POINT_MASS Для обработки столкновений в класс d3d_point_mass нужно внести только незначительные изменения. Новая версия определения класса приведена в листинге 8.1.
208 Глава 8 Листинг 8.1. Версия класса d3d_point_mass для обработки столкновений 1 class d3d_point_mass 2 { 3 private: 4 d3d_mesh objectMesh; 5 6 scalar mass; 7 vector_3d centerOfMassLocation; 8 vector_3d linearVelocity; 9 vector_3d linearAcceleration; 10 vector_3d sumForces; 11 12 scalar radius; 13 scalar coefficientOfRestitution; 14 15 D3DXMATRIX worldMatrix; 16 17 public: 18 d3d_point_mass() ; 19 20 bool LoadMesh( 21 std::string meshFileName); 22 23 void Mass ( 24 scalar massValue); 25 scalar Mass(void); 26 27 void Location( 28 vector_3d locationCenterOfMass); 29 vector_3d Location(void); 30 31 void LinearVelocity( 32 vector_3d newVelocity); 33 vector_3d LinearVelocity(void); 34 35 void LinearAcceleration( 36 vector_3d newAcceleration); 37 vector_3d LinearAcceleration(void); 38 39 void Force( 40 vector_3d sumExternalForces); 41 vector_3d Force(void); 42 43 void BoundingSphereRadius( 44 scalar sphereRadius); 45 scalar BoundingSphereRadius(void);
Столкновения материальных точек 209 46 47 void Elasticity(scalar elasticity); 48 scalar Elasticity(void); 49 50 bool Update( 51 scalar changelnTime); 52 bool Render(void); 53 }; Из строк 12 и 13 листинга 8.1 видно, что в класс d3d_point_mass добавлены новые private-элементы данных, хранящие радиус ограничивающей сферы и коэффициент восстановления. В строках 43-48 содержатся прототипы методов, предназначенных для чтения и записи значений этих элементов. За исключением добавления этих элементов данных и методов, класс d3d_point_mass остался неизменным. ПОДГОТОВКА ИМИТАЦИИ Код, непосредственно подготавливающий имитацию, находится в файле ParticleBounce.срр на компакт-диске. Код функции Gamelnitiali- zation(), в которой задаются начальные условия имитации, приведен в листинге 8.2. В целом эта функция схожа с аналогичной функцией из главы 7 «Динамика материальных точек». Листинг 8.2. Подготовка имитации столкновения двух шариков 1 bool GamelnitializationO 2 { 3 // Загружаем сетчатую модель первого шарика. 4 allParticles[0].LoadMesh("bowlball.x"); 5 6 // Задаем массу первого шарика. 7 allParticles[0].MassA0); 8 9 // Задаем коэффициент восстановления первого шарика. 10 allParticles[0].Elasticity@.9f) ; 11 12 // Задаем радиус ограничивающей сферы. 13 allParticles[0].BoundingSphereRadius@.75f); 14 15 // Делаем все свойства второго шарика такими же, как у первого. 16 allParticles[l]=allParticles[0]; 17 18 // Задаем начальное местоположение первого шарика. 19 allParticles[0].Location(vector_3d(-5.Of,0.0,0.0)); 20 21 // Задаем начальное местоположение второго шарика. 22 allParticles[1].Location(vector_3d@.0,-5.Of,0.0));
210 Глава 8 23 24 // Задаем начальные силы, действующие на шарики. 25 allParticles[0].Force(vector_3dB.Of,0.0,0.0)); 26 allParticles[l].Force(vector_3d@.0,2.Of,0.0)) ; 27 28 // 29 // Задаем рассеянный направленный свет. 30 // 31 D3DLIGHT9 light; 32 ZeroMemory( Slight, sizeof(light) ); 33 light.Type = D3DLIGHT_DIRECTIONAL; 34 35 D3DXVECTOR3 vecDir; 36 vecDir = D3DXVECTOR3@.0f, -l.Of, 1.Of); 37 D3DXVec3Normalize((D3DXVECTOR3*)Slight.Direction,SvecDir); 38 39 // Задаем цвет рассеянного света 40 light.Diffuse.r = l.Of; 41 light.Diffuse.g = l.Of; 42 light.Diffuse.Ь = l.Of; 43 light.Diffuse.a = l.Of; 44 theApp.D3DRenderingDevice()->SetLight( 0, Slight ); 45 theApp.D3DRenderingDevice()->LightEnable( 0, TRUE ); 46 theApp.D3DRenderingDevice()->SetRenderState( 47 D3DRS_DIFFUSEMATERIALSOURCE, 48 D3DMCS_MATERIAL) ; 49 50 return (true); 51 ) Эта версия функции GameInitialization() начинается с загрузки сетчатой модели первой материальной точки. Используется та же модель шара для боулинга, что и в главе 7. Масса шарика задается равной 10 килограммам. В строке 10 листинга 8.2 коэффициент восстановления шарика задается равным 0.9 — это намного больше, чем у реального шара для боулинга. Подсказка Попробуйте скомпилировать и запустить программу несколько раз, меняя значение коэффициента восстановления. В строке 13 листинга 8.2 задается радиус ограничивающей сферы для первого шарика. Строка 16 копирует все параметры первого шарика для инициализации второго. Поэтому оба шарика используют одну и ту же сетчатую модель, имеют одинаковые массы, коэффициенты восстановления и радиусы ограничивающих сфер.
Столкновения материальных точек 211 Далее функция Gamelnitialization () задает начальное местоположение первого шарика - он расположен за левым краем окна программы. В строке 22 задается начальное местоположение второго шарика - этот шарик расположен за нижним краем окна. В строках 25-26 задаются начальные силы, под воздействием которых шарики начинают двигаться к началу координат (центру окна) с одинаковой скоростью. Это гарантирует, что произойдет столкновение. Оставшаяся часть функции GameInitialization() настраивает освещение - так же, как это делалось в главе 7. ОБНОВЛЕНИЕ КАДРОВ Функция UpdateFrame () теперь должна будет проверять, нет ли столкновений между движущимися шариками. Если столкновение произошло, она должна будет вычислить силы, действующие на шарики. Эти вычисления я выделил в отдельную функцию, которая рассматривается в следующем разделе. А пока посмотрите на листинг 8.3, в котором приведена версия функции UpdateFrame (), обнаруживающая столкновения. Листинг 8.3. Функция UpdateFrame(), обнаруживающая столкновения 1 bool UpdateFrame() 2 { 3 // Создаем матрицу отображения - как в предыдущих примерах. 4 D3DXVECTOR3 eyePoint@.Of,3.Of,-5.Of); 5 D3DXVECTOR3 lookatPoint@.Of,0.Of,0.Of); 6 D3DXVECTOR3 upDirection@.Of,1.Of,0.Of); 7 D3DXMATRIXA16 viewMatrix; 8 D3DXMatrixLookAtLH(SviewMatrix, &eyePoint, &lookatPoint, 9 SupDirection); 10 theApp.D3DRenderingDevice()-> 11 SetTransform(D3DTS_VIEW,SviewMatrix); 12 13 // Создаем матрицу проецирования - как в предыдущих примерах. 14 D3DXMATRIXA16 projectionMatrix; 15 D3DXMatrixPerspectiveFovLH{SprojectionMatrix, 16 D3DX_PI/4,1.0f,1.0f,100.0f); 17 theApp.D3DRenderingDevice() 18 ->SetTransform(D3DTS_PROJECTION,SprojectionMatrix); 19 20 // Эта инициализация выполняется только один раз. 21 static bool forceApplied = false; 22 static vector_3d noForce@.0,0.0,0.0); 23 24 // Если силы еще не прикладывалась... 25 if (!forceApplied) 26 { 27 forceApplied = true;
212 Глава 8 28 } 29 // Иначе силы уже прикладывалась... 30 else 31 { 32 // Делаем их нулевыми. 33 allParticles[0].Force(noForce); 34 allParticles[1].Force(noForce); 35 } 36 37 // 38 // Проверяем, есть ли столкновения. 39 // 40 // Находим вектор расстояния между шариками. 41 vector_3d distance = 42 allParticles[0].Location() - allParticles[1].Location(); 43 scalar distanceSquared = distance.NormSquared(); 44 45 // Находим квадрат суммы радиусов шариков. 46 scalar minDistanceSquared = 47 allParticles[0].BoundingSphereRadius() + 48 allParticles[1].BoundingSphereRadius(); 49 minDistanceSquared *= minDistanceSquared; 50 51 // Изменяйте значения между 0 и 1, чтобы добиться плавной анимации. 52 scalar timelnterval = 1.0; 53 54 // Если произошло столкновение... 55 if (distanceSquared < minDistanceSquared) 56 { 57 // Отреагировать на столкновение. 58 HandleCollision(distance,timelnterval); 59 } 60 61 allParticles[0].Update(timelnterval); 62 allParticles[1].Update(timelnterval); 63 64 return (true); 65 ) В строках 4-18 листинга 8.3 функция UpdateFrame () выполняет стандартные операции, необходимые для работы с Direct3D. В строке 21 объявляется статическая переменная forceApplied, которая используется так же, как в главе 7. В строке 22 объявляется статическая переменная no- Force типа vector_3d, которая используется для инициализации векторов сил в строках 33-34. Поиск столкновений начинается в строке 41. Чтобы определить, произошло ли столкновение, функция UpdateFrame () вычисляет квадрат расстояния между центрами шариков. Это делает код из строк 41-43. Далее в строках 46-48 вычисляется сумма
Столкновения материальных точек 213 радиусов шариков, которая в строке 49 возводится в квадрат. В строке 55 полученные значения используются, чтобы определить, произошло ли столкновение. Если столкновение произошло, то в строке 58 вызывается функция HandleCollision (). Как вы вскоре увидите, эта функция вычисляет силы, действующие на материальные точки. Когда в строках 61-62 вызывается метод d3d_point_mass: : Update (), на шарики действуют силы столкновения. ОБРАБОТКА СТОЛКНОВЕНИЙ Силы, возникающие при столкновении, вычисляет функция Handle- Collision (). Это вспомогательная функция, которая не обязательно должна присутствовать в платформе физического моделирования. Код этой функции содержится в файле ParticleBounce. срр. Он приведен в листинге 8.4. Листинг 8.4. Функция HandleCollision() 1 void HandleCollision( 2 vector_3d separationDistance, 3 scalar changeInTime) 4 { 5 // 6 // Находим скорости объектов после столкновения. 7 // 8 /* Сначала нормализуем вектор расстояния, поскольку он 9 перпендикулярен к столкновению. */ 10 vector_3d unitNormal = 11 separationDistance.Normalize(FLOATING_POINT_TOLERANCE); 12 13 /* Вычисляем проекции скоростей в направлении, перпендикулярном 14 направлению столкновения. */ 15 scalar velocity! = 16 allParticles[0].LinearVelocity<).Dot(unitNormal); 17 scalar velocity2 = 18 allParticles[1].LinearVelocity().Dot(unitNormal); 19 20 // Находим средний коэффициент восстановления. 21 scalar averageE = (allParticles[0].Elasticity() * 22 allParticles[1].Elasticity()) / 2; 23 24 // Вычисляем скорости после столкновения. 25 scalar finalVelocityl = 26 (((allParticles[0].Mass() - 27 (averageE * allParticles[1].Mass())) * velocityl) + 28 (A + averageE) * allParticles[1].Mass() * velocity2)) / 29 (allParticles[0]-Mass() + allParticles[1].Mass()); 30 scalar finalVelocity2 =
214 Глава 8 31 (((allParticles[l].Mass() - 32 (averageE * allParticles[0].Mass())) * velocity2) + 33 (A + averageE) * allParticles[0].Mass() * velocityl)) / 34 (allParticles[0].Mass() + allParticles[1].Mass()); 35 allParticles[0].LinearVelocity( 36 (finalVelocityl - velocityl) * unitNormal + 37 allParticles[0].LinearVelocity()); 38 allParticles[1].LinearVelocity( 39 (finalVelocity2 - velocity2) * unitNormal + 40 allParticles[1].LinearVelocity()); 41 42 // 43 // Преобразуем скорости в ускорения. 44 // 45 vector_3d accelerationl = 46 allParticles[0].LinearVelocity() / changelnTime; 47 vector_3d acceleration = 48 allParticles[1].LinearVelocity() / changelnTime; 49 50 // Находим силы, действующие на объекты. 51 allParticles[0].Force( 52 accelerationl * allParticles[0].Mass()); 53 allParticles[1].Force( 54 acceleration2 * allParticles[1].Mass()); 55 } Задачу нахождения сил в столкновении можно решать по-разному. Ранее в этой главе мы использовали работу и законы сохранения импульса и энергии, чтобы найти скорости объектов после столкновения. Если мы учтем, что каждый кадр соответствует определенному интервалу времени, мы можем применить полученные формулы, чтобы вычислить изменение скорости в течение этого интервала. А это и есть ускорение - изменение скорости, деленное на изменение времени. Знание ускорения материальной точки позволяет нам применить формулу F = та. Эта формула действительно пригодится нам при решении многих задач. Функция HandleCollision () вычисляет скорости материальных точек после столкновения по выведенным нами ранее формулам. Вот эти формулы: vi=vi+(%-%)" V2=V2+(V2p- V2p)n Чтобы использовать эти формулы, нужно найти единичный вектор, направленный вдоль линии взаимодействия объектов при столкновении. Функция HandleCollision () находит этот вектор, вызывая метод vec-
Столкновения материальных точек 215 tor_3d: :Normalize () в строках 10-11. Вектор расстояния передается функции HandleCollision() в параметре separationDistance из функции UpdateFrame (). Найдя единичный нормальный вектор, функция HandleCollision () скалярно умножает на него векторы скоростей объектов. Это позволяет найти компоненты векторов скоростей в направлении взаимодействия тел при столкновении. Чтобы найти скорости объектов после столкновения, функция HandleCollision () должна использовать коэффициент восстановления. Для обеспечения максимальной универсальности класс d3d_point_mass позволяет каждому объекту хранить свой коэффициент восстановления. Это позволяет делать некоторые объекты более упругими, чем другие. Однако при столкновении объектов используется только один коэффициент восстановления, поэтому функция HandleCollision () находит средний коэффициент восстановления пары объектов и использует его в расчетах. Это делается в строках 21-22. Подсказка Для получения более точных результатов можно использовать взвешенные значения коэффициентов, пропорциональные массам объектов, участвующих в столкновении. Скорости объектов после столкновения вычисляются в строках 24-40. Найдя эти скорости, функция HandleCollision () может найти ускорения тел вследствие столкновения. Это делается в строках 45-48. Из ускорений функция HandleCollision () определяет силы, действующие на объекты, используя формулу F = та. РЕНДЕРИНГ КАДРОВ В главе 7 вы видели, как просто выполнять рендеринг кадров. Если мы правильно смоделировали всю физику и использовали эту физику для нахождения матрицы перемещения для Direct3D, то рендеринг сложности не представляет. В этой главе в функцию, выполняющую рендеринг, добавлена только одна строка. Взгляните на листинг 8.5. Листинг 8.5. Рендеринг столкновения шариков 1 bool RenderFrame() 2 { 3 allParticles[0].Render(); 4 allPartides [1] .Render () ; 5 return (true); 6 }
216 Глава 8 Единственное, что нужно сделать функции RenderFrame (), - вызвать метод d3d_point_mass: : Render () для двух объектов, а не для одного. Ничего сложного. Итоги В этой главе вы узнали несколько способов обнаружения столкновений между объектами. Да, об этой теме можно говорить очень долго, но для начала этого хватит. Мы много говорили о физике столкновений. Изложенное в этой главе нам пригодится, когда мы будем рассматривать столкновения твердых тел. И, наконец, мы создали программу, моделирующую столкновение двух шариков. Изучая следующие главы, вы не раз удивитесь, насколько полезна несложная физика, рассмотренная в этой главе.
Глава 9 Динамика твердых тел В компьютерных играх часто присутствуют объекты более сложной формы, чем рассматривавшиеся нами до сих пор сферы и треугольники. Вам, вероятно, захочется моделировать автомобили, которые поворачиваются и, возможно, переворачиваются, драконов, которые могут парить и пикировать, и персонажей, способных стрелять, прыгать и бросать предметы. Цель этой главы - создать общую физическую модель для работы со сложными твердотельными объектами такого рода. Твердые тела Золотое правило моделирования (и вообще физических расчетов) - упрощай. Нужно получать как можно больше от максимально упрощенной модели. Любой обычный объект чрезвычайно сложен. Он состоит из астрономического количества атомов, взаимодействующих между собой по сложным законам. Смоделировать взаимодействие всех атомов для объектов, которые мы можем видеть невооруженным глазом, невозможно. Поэтому мы поступим следующим образом. Вероятно, вы заметили, что большая часть объектов вокруг вас сохраняет свою форму с течением времени. Ваша микроволновая печь выглядит сегодня так же, как вчера, если только она не расплавилась от перегрузки. Все объекты, сохраняющие свою форму, мы будем называть твердыми телами (rigid bodies) и предположим, что эти объекты будут сохранять свою форму в течение всего времени их существования. Любой реальный объект не сохраняет свою форму абсолютно неизменной. Теннисный мяч сплющивается при ударе ракеткой, а потом восстанавливает свою форму. Если вы ударите микроволновую печь кувалдой или расплавите ее, она (вероятно, необратимо) изменит свою форму. Но в играх многие объекты можно моделировать как твердые тела. Твердые объекты удобно представлять в виде наборов материальных точек, как на рисунке 9.1. Гантель с рисунка 9.1 состоит из двух соединенных между собой материальных точек. Мы пока проигнорируем массу перемычки. У каждой из этих материальных точек есть масса, и на нее могут действовать силы, включая и воздействия других материальных точек.
218 Глава 9 Рис. 9.1. Представление твердого объекта как набора материальных точек В твердых телах расстояния между материальными точками является постоянным. Это значит, что расположение любой материальной точки относительно других в пределах твердого тела будет неизменным. Другими словами, объект сохраняет свою форму. Как выясняется, такое представление весьма полезно на практике. Оно лежит в основе ряда концепций, сильно упрощающих моделирование объектов реального мира. Центр масс Брошенный мяч опишет в воздухе плавную кривую - параболу, как показано на рисунке 9.2. Траектория движения брошенного томагавка выглядит значительно сложнее, как показано на рисунке 9.3. Но если присмотреться, станет ясно, что одна из точек томагавка движется по той же параболе. Эта точка называется центром массы (center of mass). Посмотрим, сможем ли мы найти ее местоположение с помощью математики. / / / / / ч \ \ \ / \ / \ \ Рис. 9.2. Мяч в полете движется по параболе
Динамика твердых тел 219 Центр массы / # Томагавк вращается \ / вокруг центра массы \ / / \ Рис. 9.3. У брошенного томагавка по параболе движется центр массы Объекты состоят из множества маленьких частиц, каждая из которых обладает определенной массой и подвергается воздействию определенных сил. Силу, действующую на i-ю частицу, можно представить в таком виде: Ш:ё2х- d2m.x- F=m;a; - ' ' - J ' dt2 dt2 Здесь nij - масса i-й частицы, а х; - ее местоположение, как показано в следующем уравнении: F = 2 Fi = 2 d2miXi/dt2 = d2/dt2 J miXi i i i Это просто другая форма записи уравнения F = та. В данном случае мы представили его с помощью производных — ускорение есть вторая производная от перемещения по времени. Если определить центр масс как Хст в формуле: Хст = !/М 2 miXi где М - общая масса твердого тела, то общую силу, действующую на тело, можно выразить несложной формулой: г _ Md2Xcm dt2
220 Глава 9 Это аналог второго закона Ньютона для материальных точек. Из этой формулы следует, что с точки зрения общего перемещения тела и общей действующей на него силы твердое тело можно рассматривать как материальную точку, находящуюся в его центре масс. Нам остается разобраться с вращениями твердых тел. Поступательное движение твердого тела моделируется точно так же, как и движение материальных точек, которое мы подробно рассмотрели в предыдущих двух главах. Вращение можно рассматривать независимо от поступательного движения. Подсказка За счет совмещения центра массы тела и центра сетчатой модели часто удается сильно упростить вычисления. Вращение двумерных твердых тел Итак, мы разобрались с общим местоположением и скоростью твердого тела. Нужно просто рассматривать центр массы как материальную точку. Это просто. А как насчет вращательного движения? Вероятно, вы не удивитесь, узнав, что с ним все гораздо сложнее. Давайте сначала разберемся с вращением двумерных твердых тел на плоскости - чтобы сделать это, нам не придется погружаться в замысловатую математику. Поскольку объекты могут двигаться только в одной плоскости, они могут вращаться только вокруг осей, перпендикулярных этой плоскости. На рисунке 9.4 показано твердое тело, ведущее себя таким образом. Центр массы Томагавк вращается Рис. 9.4. Твердое тело, вращающееся в вокруг центра массы плоскости вокруг оси z Твердые тела, которые могут двигаться только в одной плоскости, встречаются весьма часто даже в ЗБ-играх. Если вы толкаете объекты по полу, их движение можно моделировать 2Б-механикой, если они не отрываются от поверхности. Шестеренки и колеса тоже можно представлять в виде двумерных твердых тел, если они не опрокидываются. Математика, необходимая для моделирования двумерных твердых тел, куда проще математики, необходимой для моделирования трехмерных твердых тел, поэтому ее стоит использовать везде, где это возможно.
Динамика твердых тел 221 Для объектов, которые могут двигаться только в одной плоскости, вращение можно описывать одним скаляром — в, как показано на рисунке 9.5. Угол мы будем измерять в радианах. Конечное L Центр массы положен!/ юе L Цент ние '^ ' I Начальное Рис. 9.5. Вращение двумерного твердогс углом в твердого тела можно описывать одним положение ^ По аналогии с одномерной кинематикой мы можем определить угловую скорость а> и угловое ускорение а: (о = d0/dt а = <b/dt = d20/dt2 Угловая скорость измеряется в радианах в секунду (рад/с), а угловое ускорение - в радианах в секунду за секунду (рад/с2). Материальные точки в двумерном твердом теле В этом разделе мы изучим следующий вопрос: если у нас есть данные о твердом теле (его местоположение, скорость, ускорение, угловые скорость и ускорение), то, как найти скорость и ускорение частицы в твердом теле? Это важный вопрос, поскольку полезность модели твердого тела основана на возможности приложения внешних сил к разным ее частям одновременно. Рассмотрим локальную систему координат, движущуюся с той же скоростью, что и центр масс тела. Твердое тело поворачивается на угол в. Сосредоточим свое внимание на одной точке твердого тела, находящейся на расстоянии г от центра масс. При вращении тела она проходит путь по дуге длиной arcLength. Согласно определению радиана, в = arcLength/r . Если продифференцировать это выражение по времени, мы получим выражение для угловой скорости:
222 Глава 9 d0/dt = d/dt (arcLength/r) или о) = A/r) d arcLength/dt Здесь darcLength/dt есть угловая скорость, показанная на рисунке 9.6. Для твердого тела угловая скорость есть общая скорость частицы в локальной системе координат, поэтому мы обозначим ее v. Угловая UeHTP массы Рис. 9.6. Угловая скорость частицы в двумерном твердом теле Можно выделить 1 / г из выражения производной, поскольку расстояние от частицы до центра масс в твердом теле есть величина постоянная. Вообще все расстояния между частицами в твердом теле есть величины постоянные, и они не изменяются, когда мы берем производную. v = га) Чтобы определить тангенциальное ускорение (tangential acceleration) частицы, нужно еще раз продифференцировать это выражение по времени: dv/dt = rdft)/dt at = ra Обратите внимание - я сказал: «тангенциальное ускорение». Есть и еще одно ускорение - центростремительное. Как показано на рисунке 9.7, тангенциальное ускорение изменяет величину вектора скорости частицы, а центростремительное ускорение изменяет его направление.
Динамика твердых тел 223 Пока мы в основном сосредоточимся на тангенциальном ускорении, но постепенно мы разберемся с обоими. Центр массы Рис. 9.7. Тангенциальное и центростремительное ускорение Центростремительному ускорению соответствует центростремительная сила. Эта сила заставляет частицу отклоняться от прямолинейной траектории движения. Если бы вы сидели на вращающейся частице, вы бы чувствовали, что на вас действует сила, сбрасывающая вас с частицы, действующая в направлении, обратном направлению центростремительной силы. Вы бы почувствовали эту силу, поскольку вы не прикреплены намертво к вращающейся частице, и ваше тело стремится двигаться по прямой. Сила, противодействующая центростремительной, называется центробежной (centrifugal force). Вот краткий вывод формулы для нахождения центростремительного ускорения с помощью конечных разностей. За незначительный интервал времени At центростремительное ускорение изменяет направление вектора скорости частицы, но не его величину, как показано на рисунке 9.8. На рисунке изменение скорости из-за действия центростремительного ускорения обозначено Av. Av равно произведению центростремительного ускорения и At: Av = acAt Изменение в направлении вектора тангенциальной скорости происходит при круговом движении частицы. Точнее говоря, оно происходит при движении частицы по дуге. Помните величину arcLength, использовавшуюся ранее в этой главе? Она как раз и обозначает дугу, пройденную частицей. Это значит, что мы можем записать: AarcLength/r = Av/v Здесь тангенциальная скорость определена как производная длины дуги, поэтому с помощью конечных разностей мы можем записать: Тангенциальное ускорение AarcLength = vAt
224 Глава 9 Начальная тангенциальная скорость А Конечная тангенциальная скорость Центростремительное ускорение Рис. 9.8. Изменение угловой скорости под действием центростремительного ускорения Чтобы найти центростремительное ускорение, выполним подстановки для AarcLength и Av: ас = Av/At = vAarcLength/rAt = = v2At/rAt = v2/r Это выражение позволяет нам найти центростремительное ускорение частицы, зная ее угловую скорость и расстояние от нее до центра массы твердого тела. Вращающий момент и момент инерции Ручки на дверях размещаются как можно дальше от петель по определенной причине. Попытайтесь закрыть дверь, толкая ее в точке вблизи петель. Вам это удастся (скорее всего), но ценой заметных усилий. Эту особенность твердых тел описывает вращающий момент (torque). Он обозначается г и определяется следующим образом: т = г X F Здесь г - расстояние от центра масс твердого тела до точки, к которой мы прикладываем силу F. Если объект может двигаться только в одной плоскости, то момент всегда указывает в одном направлении, и мы можем найти его, обойдясь скалярами: т = rFsin@) Здесь в есть угол между векторами г и F. Если сила направлена по касательной к окружности (как сила трения о землю для катящегося колеса), можно даже записать: х = rF,
Динамика твердых тел 225 Теперь, определив концепцию, попробуем связать ее с той механикой, которую мы уже рассматривали ранее. Рассмотрим вращающий момент i-й материальной точки в твердом теле. Ч = riFti Тангенциальная сила, действующая на частицу, равна произведению массы этой частицы на ее тангенциальное ускорение (согласно второму закону Ньютона): Fti = miati Поэтому вращающий момент частицы можно записать в виде: Ч = rimiati В предыдущем разделе мы выяснили, что а^ = га, где а - угловое ускорение. Тогда ■q = г^ща Чтобы получить общий вращающий момент твердого тела, нужно просуммировать вращающие моменты всех его частиц: Сумма в скобках - это момент инерции (moment of inertia), обычно обозначаемый I. 7 = 2 ч = а 2 ri2mi = i i = B Ч2тд а i х = la Это выражение представляет собой аналог второго закона Ньютона для вращательного движения. Поскольку второй закон Ньютона для вращения выражен так же, как и для материальных точек, у уравнений будут одинаковые решения. Это очень удобно. ВЫЧИСЛЕНИЕ МОМЕНТА ИНЕРЦИИ Использование второго закона Ньютона для вращательного движения позволяет вычислять момент инерции для любого твердого тела. Для многих форм твердых тел можно просто просуммировать моменты инерции составляющих их частиц по формуле: I = 2 ri2mi i Моменты инерции будут вычисляться тем точнее, чем больше частиц как можно меньшего размера мы примем во внимание.
226 Глава 9 Если сделать частицы бесконечно малыми, а их общее количество - бесконечно большим, то сумма в предыдущей формуле превратится в интеграл, который можно взять, чтобы найти момент инерции: I = J r2dm На рисунке 9.9 перечислены некоторые широко распространенные формы тел и соответствующие им моменты инерции, найденные по этой формуле. Ось Обруч относительно оси цилиндра l = MR2 (а) Ось Пустотелый цилиндр (или кольцо) относительно оси цилиндра I = M(R-,2 + R22)/2 (b) Ось Однородный цилиндр (или диск) относительно оси = (MR2)/2 (с) Однородный цилиндр (или диск) относительно центрального диаметра l=(MR2)/4+(MI2)/12 (d) Ось .ЦП Тонкий стержень относительно оси, проходящей через центр \ массы и перпендикулярной длине I = (М|2)/12 (е) Тонкий стержень относительно оси, проходящей через один из концов стержня и перпендикулярной длине I = (М|2)/з (f) Однородная (заполненная) сфера относительно любого диаметра \r I = BMR2)/5 (g) Тонкая сферическая оболочка относительно любого диаметра I = BMR2)/3 (h) Ось Обруч относительно любого диаметра I = (MR2)/2 Ось б Обруч относительно любой касательной линии I = CMR2)/2 Ш Рис. 9.9. Моменты инерции для некоторых форм твердых тел
Динамика твердых тел 227 Зная момент инерции твердого тела относительно какой-нибудь оси, можно найти его момент инерции относительно любой другой оси, параллельной этой. Это свойство описывается теоремой Гюйгенса, известной также как теорема о параллельных осях (parallel axis theorem): I = Icm + Mh2 Здесь Icm - момент инерции твердого тела относительно его центра массы, М - масса этого тела, h - расстояние до новой оси, как показано на рисунке 9.10. Рис. 9.10. Теорема Гюйгенса На рисунке 9.10 показана гантель, вращающаяся вокруг оси, не проходящей через ее центр массы. Гантель - это твердое тело, состоящее из двух тяжелых элементов, которые можно рассматривать как материальные точки. На примере левой сферы рисунок демонстрирует, что материальные точки могут вращаться вокруг оси, не проходящей через центр массы. Эта ось находится на расстоянии h от оси, проходящей через центр массы гантели. С помощью теоремы Гюйгенса можно найти момент инерции твердого тела, составленного из твердых тел более простой формы. Моменты инерции суммируются, если они вычислены относительно одной и той же оси, поэтому нужно применять теорему Гюйгенса, чтобы преобразовать моменты инерции отдельных частей тела относительно их осей в моменты инерции относительно общей оси вращения тела. ПРИМЕР ПРИМЕНЕНИЯ ТЕОРЕМЫ ГЮЙГЕНСА Попробуем найти момент инерции относительно центра масс для тела, изображенного на рисунке 9.11. Это твердое тело состоит из трех элементов: двух однородных сфер и обруча, расположенных так, как показано на рисунке. Масса каждой сферы равна 200 кг, а радиус -1м. Радиус обруча — тоже 1 м, а его масса — 100 кг. Все твердое тело может двигаться только в одной плоскости, поэтому мы будем рассматривать его как двумерное.
228 Глава 9 Замечание Мы будем часто использовать упрощенные модели вроде этой для расчета взаимодействия твердых тел. Чтобы объекты вели себя реалистично в играх, их физические модели должны соответствовать сетчатым моделям, изображающим эти объекты. Рис. 9.11. Твердое тело, состоящее из нескольких простых форм Первое, что нам нужно сделать, - найти центр масс. Вот определение центра масс: Хст = A/М) 2 miXi i Поместим начало системы координат в центр обруча. Общая масса М равна 200 кг + 200 кг + 100 кг = 500 кг. По рисунку 9.11 можно найти центр массы - его координаты Хст (по оси х) и Ycm (по оси у): Хст = A/М) 2 miXi i = B00 кг • 0 м+ 200 кг • 0 м +100 кг • 0 м ) / 500 кг = 0 м Ycm = A/М) 2 т1У1 i = B00 кг • (-2 м) + 200 кг • 2 м +100 кг • 0 м ) / 500 кг = 0 м Результат, который мы получили, вероятно, был очевиден для вас: центр массы рассматриваемой фигуры расположен в центре обруча, и в выбранной нами системе координат его координаты равны @; 0).
Динамика твердых тел 229 Теперь с помощью теоремы Гюйгенса можно найти моменты инерции каждого элемента тела относительно оси, проходящей через центр массы тела. Расстояния от центра массы каждого элемента до центра массы тела приведены в таблице 9.1. Таблица 9.1. Расстояния от центра массы каждого элемента до центра массы тела Элемент Расстояние Нижняя сфера 2 м Верхняя сфера 2 м Обруч 0 м Нам понадобятся моменты инерции каждого элемента относительно его центра массы. Момент инерции однородной сферы относительно любого диаметра равен B/5)MR2, поэтому момент инерции каждой сферы равен 80 кг»м2. Момент инерции обруча относительно центральной оси равен MR2, поэтому в нашем примере момент инерции обруча равен 100 кг»м2. Это вся нужная нам информация. Просто применим теорему Гюйгенса. Для двух сфер результаты будут одинаковыми: Sphere = hm + Mh2 = 80 кг«м2 + 200 кг B мJ = 880 кг»м2 Для обруча: Ihoop = Icm + Mh2 = 100 кг«м2 + 100 кг @ мJ = 100 кг»м2 Общий момент инерции тела равен сумме моментов инерции его элементов: 1 = 2Isphere + Ihoop = 2(880 кг-м2) + 100 КГ-М2 = 1 860 КГ-м2 Твердые тела в 3D Разобравшись в поведении двумерных твердых тел, можно начать переход к трехмерным. Хотя можно создать множество игр, обходясь только двумерными твердыми телами, все больше и больше игр используют трехмерные. Например, нельзя создать авиасимулятор без трехмерных твердых тел. Математика, используемая для описания поведения трехмерных твердых тел, довольно замысловата, и мы будем разбираться в ней шаг за шагом. Начнем с изучения вращения трехмерного твердого тела вокруг
230 Глава 9 произвольной оси. В любой момент времени это твердое тело будет вращаться вокруг выбранной оси с определенной угловой скоростью, как показано на рисунке 9.12. Это вращение можно описать с помощью направленного вдоль оси вращения тела вектора угловой скорости со, длина которого равна угловой скорости. Рис. 9.12. Вектор угловой скорости описывает угловую скорость вращения вокруг произвольной оси Замечание Почему мы начали с угловой скорости, а не с вектора, определяющего ориентацию, аналогично скаляру ориентации G в двумерном случае? Потому что связь между вектором ориентации и угловой скоростью в этом случае не такая простая, как в двумерном. Собственно говоря, ш, вектор угловой скорости, не является производным какого-то другого вектора. Почти все параметры (угловая скорость, угловое ускорение и вращающий момент) определяются похожими методами, поэтому мы начнем с них. Вектор углового ускорения а есть производная угловой скорости по времени: а = cL»/dt Вроде бы неплохо. Эта формула выглядит привычно. Мы просто применили в ней векторы. Следующее, что мы сделали для двумерного тела, - связали угловые скорость и ускорение со скоростями и ускорениями частиц, образующих твердое тело. Попробуем сделать то же самое и для трехмерного тела. Скорость v частицы, расположенной в точке г относительно центра массы тела, будет определяться соотношением: v = со X г
Динамика твердых тел 231 Вращение вокруг произвольной оси можно свести к двумерному случаю, спроецировав вектор г на плоскость, перпендикулярную оси вращения. Длина вектора будет равна г sin#, где в - угол между двумя векторами. В предыдущем разделе мы вывели формулу для скорости в двумерном случае: v = сот. Эта формула соответствует длине вектора, получаемого в результате векторного произведения. |а х b| = ab sin@) |v| = \со X г| |v| =cor sin(9) Вектор скорости по определению должен быть перпендикулярным и радиусу, и оси ориентации. Теперь можно найти ускорение материальной точки в твердом теле, взяв производную по времени от вектора скорости: а = dv/dt = d(ft>xr)/dt = = (dft>/dt)xr + со x(dr/dt) = axr+cox(coxr) Итак, первый элемент в полученном результате - тангенциальное ускорение, а второй - центростремительное. Попробуйте сравнить этот результат с выражениями для двумерного случая: at = а х г ас=£о х(»хг) Замечание Все изложенное может показаться малопонятным, если вы не знакомы с векторным анализом. Возможно, оно останется малопонятным, даже если вы с ним знакомы. Я признаю, что недостаточно храбр, чтобы пытаться излагать в этой книге основы векторного анализа, но, если вы хотите с ним познакомиться, попробуйте, например, книгу Н. М. Schey «Div, grad, curl and all that». Если вас интересует более подробное рассмотрение тех проблем, которые мы разбираем здесь, попробуйте почитать книги по теоретической механике, например, Herbert Goldstein «Classical Mechanics».' Получив векторные выражения для скорости и ускорения частиц в системе координат, неподвижной по отношению к твердому телу, мы можем легко найти скорость и ускорение в глобальной системе координат. Достаточно прибавить их к скорости и ускорению центра масс твердого тела: vworld = vcm + V х r aworld = acm + со X r + со х (со х г) 1 Отечественному читателю доступнее Смирнов В. И. «Курс высшей математики» и Голубева О. В. «Теоретическая механика». Обе книги переиздавались много раз и выложены в Интернете, например, на сайте lib.mexmat.ru. — (Прим. переев).
232 Глава 9 Вращающий момент в 3D Определение вращающего момента для 3D у нас уже есть - оно такое же, как и для 2D: т = г X F Это, конечно, неплохо - по сравнению с предыдущим разделом. Но не спешите радоваться - посмотрите, что будет дальше. Мы применим тот же прием, что и раньше - попытаемся найти вращающий момент £-й частицы твердого тела. Сначала мы подставим выражение для прикладываемой силы F: Fi = miati Тогда ri=ri х miati= m^ x ati Прикладываемая сила соответствует тангенциальному ускорению. Центростремительное ускорение является результатом действия сил, определяемых структурой твердого тела. Теперь можно подставить найденное в предыдущем разделе выражение для тангенциального ускорения: ati = а х г{ Мы получим: г^= Г{ х m^i = mji-j x a x Г| Следующий шаг - просуммировать вращающие моменты всех частиц тела, чтобы найти общий вращательный момент: т = ^ Ч = 2miriXaXri i i Для двумерного случая мы просто выделили из выражения угловое ускорение и получили скалярную величину, которую назвали моментом инерции. Но в трехмерном случае все не так просто! Как выделить вектор а из нашего выражения? Посмотрим на компоненты векторов. Компоненты вектора г4 обозначим (х, у, z). Индексов i в выражении нет, чтобы оно было более компактным, но не забудьте, что мы суммируем вращающие моменты всех частиц твердого тела. Компоненты вектора а обозначим (ах, ау, az). Приготовьтесь — выражение будет то еще! г = 2 mi{[+(y2+z2)ax-xyay-xzaz] x+ i +[-xyax+(z2+x2)ay-yzaz] y+ +[-xzax-yzay+(x2+y2)az] z}
Динамика твердых тел 233 Подсказка В векторном анализе обычно стоит перепробовать все остальное, прежде чем начинать покомпонентный разбор, поскольку выражения получаются громоздкими и малопонятными. Вероятно, есть более изящный способ получения нужного нам результата. Если кто-нибудь его знает, пожалуйста, сообщите автору. Вероятно, выражение покажется вам непонятным и ни на что не похожим, но сделаем несколько замен и посмотрим, что получится. Вот эти замены: *хх = I (У^+^К i !уу = 2 (zi2+xi2)mi i Jzz = 2 (xi2+yi2)mi i !Xy = Jyx = 2 (xiYi)mi i Ixz = hx = 2 (XiZOnij i :yz = Jzy = 2 (yiZi)mi i Тогда выражение для вращающего момента примет вид х = (+ 1ххах - 1хуау - Ixzaz) х + (- Iyxax + Iyytty - Iyza2) у + (_ Xzx«x - VV + Izz«z) Z Возможно, это выражение кому-то покажется знакомым? Это выражение умножения матрицы на вектор в компонентной форме. Говоря другими словами, если мы выделим матрицу I, компоненты которой будут равны множителям при компонентах вектора, мы получим: Т = I XX I ху ух -I ZX -I УУ -I ^XZ -I zy yz Lzz г- -i ах ау Laz. = 1а
234 Глава 9 Геометрический объект, компоненты которого в некоторой координатной системе образуют квадратную матрицу, называется тензором (tensor). Соответственно, I - это тензор момента инерции. Его использование позволяет нам записать вращательный аналог для второго закона Ньютона в 3D. Замечание Хотя в книге используются похожие обозначения для векторов и тензоров, тензор I можно отличить от вектора по способу его использования в выражении. Нет операции над векторами, которая обозначается записью рядом двух векторов без знака операции между ними. Полученное нами выражение вполне корректно, но с точки зрения использования его в расчетах у него есть один недостаток. Оно работает в невращающейся системе координат, и тензор момента инерции будет изменяться от кадра к кадру, вследствие чего его нужно будет пересчитывать для каждого кадра. Поэтому вычисления будут выполняться очень медленно. Лучше получить аналог этого выражения для системы координат, вращающейся с угловой скоростью со. Это несложно сделать, применив некоторые приемы из векторного анализа. Можно связать производную по времени от вектора v в фиксированной системе координат и эту же производную во вращающейся системе координат с помощью формулы, основанной на определении производной в этой ситуации: (dv/dt)$HKC = (dv/dt)Bpan; + (со х v) Применение этой формулы к уравнению движения приведет к следующему результату: т = \а + (ft) X 1<у) В этой координатной системе тензор момента инерции будет постоянным, и его нужно вычислять только один раз. Это уравнение не такое изящное, как полученное нами выше, но вычисляться оно будет быстрее. Теорема Гюйгенса в 3D Компоненты тензора момента инерции, расположенные на его диагонали (Ixx, I , Izz), называются моментами инерции в трехмерном пространстве. Теорема Гюйгенса для них выглядит так же, как и для скалярного момента инерции: XX *УУ ~~ Wfrz) + Mh2 Icm(xz) + Mh2
Динамика твердых тел 235 Jzz = Icm(xy) + Mh2 Здесь расстояние h от центра массы измеряется в плоскости, перпендикулярной моменту инерции. Например, для момента 1хх все измеряется в плоскости yz. Существует также теорема для компонентов тензора момента инерции, не расположенных на диагонали. Эти компоненты можно записать так: *ху = Wfxy) + Mhxhy Jxz = Icm(xz) + Mnxhz Jyz = ^mtyz) + Mhyhz Здесь hx - компонент расстояния между осями по оси х, hy - такой же компонент по оси у, a hz - по оси z. Выбор осей Ранее уже упоминалось, что матрица не есть тензор момента инерции, а только его компоненты в выбранной системе координат. В системе координат нет ничего особенного. Выбор других осей х, у и z даст нам совершенно другие величины компонентов, но тензор момента инерции останется тем же. Как выясняется, всегда можно выбрать такой набор осей, у которого компоненты, не расположенные на диагонали матрицы, будут равны 0. Эта особенность существенна, если вы выполняете расчеты и преобразования вручную, но для просчитываемой на компьютере модели она не столь уж важна. Здесь об этой особенности упоминается только ради полноты изложения. Ориентация Мы рассмотрели все аспекты ЗБ-твердых тел, кроме одного. Мы можем применять концепции вращающих моментов и находить новые угловые скорости для тел любой формы, для которых мы можем найти тензор момента инерции. Нам не хватает только одного: аналога возможности находить ориентацию тела по угловой скорости в 2В-пространстве. В 2Б-пространстве ориентация определялась скаляром в, и ее можно было найти, зная <х>, с помощью метода конечных разностей: со = Лв/At Вероятно, вы надеялись, что в 3D эта задача будет решаться так же просто. Увы, со не есть производная какого бы то ни было вектора, и решить нашу задачу будет сложно. Как это сделать?
236 Глава 9 Поскольку мы не можем напрямую перейти от угловой скорости к ориентации, попробуем обходные пути. Ориентация в ЗБ-пространстве - довольно сложная тема. В играх для определения ориентации используются два метода: матрицы вращения и кватернионы. У каждого метода есть свои достоинства и недостатки. Матрицы вращения - самый простой и прямолинейный способ решения проблемы. Direct3D и другие библиотеки, например, OpenGL, использовали матрицы многие годы. Однако кватернионы могут быть более эффективными. Недостаток кватернионов - многим пользователям сложно в них разобраться. Замечание Если чувствуете в себе отвагу, я советую вам попробовать разобраться в кватернионах. В последних версиях Direct3D появилась поддержка для них. Поскольку матрицы вращения доступнее для понимания и проще в реализации, мы будем использовать именно их в оставшейся части книги. Мы уже рассматривали их весьма подробно. Вы знаете, как выглядят матрицы для вращения вокруг осей х, у и z. Вот эти матрицы: R. = 10 0 0 0 cos(q>) sin((p) 0 0 -sin((p) cos(cp) 0 0 0 0 1 R, cos(cx) 0 -sin(a) 0 0 10 0 sin(a) 0 cos(oc) 0 0 0 0 1 Rz = cos(9) sin(9) 0 0 -sin(9) cos(9) 0 0 0 0 10 0 0 0 1
Динамика твердых тел 237 Теперь нам нужно определить, как эти матрицы будут изменяться с течением времени. Для начала сделаем каждый из углов поворота зависимым от времени. Для малых промежутков времени зависимости будут линейными: <р = tuxt р = ft>yt e = a>xt Подставив эти выражения в матрицы и перемножив получившиеся матрицы, можно получить общую матрицу вращения. Не стоит тратить время и делать это вручную. Воспользуйтесь любой математической программой, например, Maple компании MapleSoft: R = RxRyRz Теперь продифференцируем R по времени. Вы обнаружите, что dR/dt = coR где со есть матрица вида С0 = О -СОг С0> (Hz О -со* -СОу СО* О Самое замечательное здесь то, что матрицу вращения нужно будет вычислить только однажды - и все эти медленно вычисляющиеся тригонометрические функции будут использоваться только один раз. Получив матрицу вращения, можно находить новую угловую скорость со и затем просто выполнять умножение матриц. Используя этот метод, надо остерегаться ошибок округления. Из-за конечной точности вычислений маленькие ошибки быстро становятся существенными. Чтобы матрица вращения постепенно не превратилась в полную кашу, нужно часто приводить ее к ортогональному виду: rtr = i Такой подход хорош для обработки поворотов в играх. В нем просто разобраться, и он использует все те же операции умножения матриц, которые мы использовали все время. Эти операции можно оптимизировать, чтобы повысить скорость. Это неплохой выбор.
238 Глава 9 Подсказка Как уже говорилось выше, кватернионы в последнее время стали очень популярным способом представления ориентации объектов в 3D. Я настоятельно рекомендую вам после прочтения этой книги изучить кватернионы. Хотя связанная с ними математика довольно замысловата, они могут быть очень полезны. Реализация твердых тел в 3D Наконец, мы добрались до этапа создания класса, который будет представлять твердые тела в 3D. После всего, что мы уже сделали, это будет несложно. Замечание Код примера программы из этой главы есть на компакт-диске в папке So- urce\Chapter09\RigidBody. Класс d3d_rigid_body Для моделирования твердого тела нужны переменные для хранения, например, массы, местоположения, скорости и суммарной действующей на него силы. Нам понадобятся переменные для хранения вращательных величин. Кроме того, с твердым телом связана сетчатая модель. Центр массы этого тела считается началом системы координат для модели. Пока такое положение нас устраивает, но в играх от него, скорее всего, придется отказаться. В сложных твердых телах центры масс почти никогда не совпадают с началами систем координат сетчатых моделей, поэтому в последующих главах мы откажемся от такого совмещения. Для обнаружения столкновений в классе используется метод ограничивающей сферы, поэтому в определении есть элемент для хранения радиуса этой сферы. Замечание Определение класса твердого тела содержится в файле PMRigidBody. h в папке Source\Chapter09\RigidBody. В листинге 9.1 приведено определение класса твердого тела — d3d rigid_body.
Динамика твердых тел 239 Листинг 9.1. Класс d3d_rigid_body 1 class d3d_rigid_body 2 { 3 private: 4 d3d_mesh objectMesh; 5 6 // Физические свойства и характеристики линейного движения. 7 scalar mass; 8 vector_3d centerOfMassLocation; 9 vector_3d linearVelocity; 10 vector_3d linearAcceleration; 11 force sumForces; 12 13 // Характеристики вращательного движения 14 angle_set_3d currentOrientation; 15 vector_3d angularVelocity; 16 vector_3d angularAcceleration; 17 vector_3d rotationalInertia; 18 vector_3d torque; 19 20 D3DXMATRIX worldMatrix; 21 22 public: 23 d3d_rigid_body(void); 24 25 bool LoadMesh( 26 std::string meshFileName); 27 28 void Mass( 29 scalar massValue); 30 scalar Mass(void); 31 32 void Location( 33 vector_3d locationCenterOfMass); 34 vector_3d Location(void); 35 36 void LinearVelocity( 37 vector_3d newVelocity); 38 vector_3d LinearVelocity(void); 39 40 void LinearAcceleration( 41 vector_3d newAcceleration); 42 vector_3d LinearAcceleration(void); 43 44 void Force( 45 force sumExternalForces); 46 force Force(void);
240 Глава 9 47 48 void CurrentOrientation( 49 angle_set_3d newOrientation); 50 angle_set_3d CurrentOrientation(void); 51 52 void AngularVelocity( 53 vector_3d newAngularVelocity); 54 vector_3d AngularVelocity(void); 55 56 void AngularAcceleration( 57 vector_3d newAngularAcceleration); 58 vector_3d AngularAcceleration(void); 59 60 void RotationalInertia(vector_3d inertiaValue); 61 vector_3d Rotationallnertia(void); 62 63 void Torque(vector_3d torqueValue); 64 vector_3d Torque(void); 65 66 bool Update( 67 scalar changelnTime); 68 bool Render(void); 69 ); Как и в классе d3d_point__mass, рассматривавшемся в предыдущих главах, в классе d3d_rigid_body есть элемент-объект класса d3d_mesh. Он объявлен в строке 4 листинга 9.1. Кроме того, в строках 7-11 объявлены элементы, предназначенные для хранения параметров линейной динамики. Элементы данных, объявленные в строках 14-18, хранят характеристики вращательной динамики твердого тела. В классе d3d_rigid_body есть методы для чтения и записи значений элементов данных. Кроме того, есть методы Update () и Render (), о назначении которых вы, вероятно, уже догадались. Пока все просто и прямолинейно. Двинемся дальше - посмотрим, как этот класс используется. Инициализация объекта класса d3d_rigid_body В примере программы из этой главы демонстрируется использование класса d3d_rigid_body. Вся логика, специфичная для этой программы, находится в файле RigidBodyTest. cpp. В этом файле содержатся функции, требуемые платформой физического моделирования. Согласно требованиям платформы, инициализация Direct3D выполняется в функции OnAppLoadO- Единственный момент, достойный внимания — в программе используется версия этой функции, отключающая моделирование освещения Direct3D. Эта версия использовалась в первых примерах программ с треугольниками. В примерах с шариками
Динамика твердых тел 241 моделирование освещения не отключалось. Из-за способа использования материалов и текстур в данном примере моделировать освещение не нужно. Замечание Чтобы увидеть этот пример в работе, скопируйте в рабочую директорию проекта файлы tiger.x и tiger.bmp, поставляющиеся вместе с SDK DirectX. Установив инструментарий SDK, перейдите на диск, на который он установлен. На диске должна быть папка DXSDK. В ней есть подпапка Samp- les\Media, в которой и находятся нужные файлы. Инициализация собственно объекта твердого тела выполняется в функции Gainelnitialization (). Код этой функции приведен в листинге 9.2. Листинг 9.2. Инициализация объекта твердого тела 1 bool Gainelnitialization () 2 { 3 // Создаем матрицу отображения - как в предыдущих примерах. 4 D3DXVECTOR3 eyePoint@.Of,3.Of,-10.Of); 5 D3DXVECTOR3 lookatPoint@.Of,0.Of,0.Of) ; 6 D3DXVECTOR3 upDirection@.Of,1.Of,0.Of); 7 D3DXMATRIXA16 tempViewMatrix; 8 D3DXMatrixLookAtLH(StempViewMatrix,SeyePoint, 9 SlookatPoint,SupDirection); 10 theApp.ViewMatrix(tempViewMatrix); 11 12 // Создаем матрицу проецирования - как в предыдущих примерах. 13 D3DXMATRIXA16 projectionMatrix; 14 D3DXMatrixPerspectiveFovLH( 15 &projectionMatrix,D3DX_PI/4,1.0f,1.0f,100.Of); 16 theApp.ProjectionMatrix(projectionMatrix); 17 18 // Загружаем сетчатую модель объекта. 19 theObject.LoadMesh("tiger.x"); 20 21 theObject.AngularVelocity(vector_3d@.0,0.0,0.0)); 22 theObject.AngularAcceleration(vectored@.0,0. 0,0.0)) ; 23 theObject.RotationalInertia(vector_3dC9.6f,39.6f,12.5f)); 24 theObject.Torque(vector_3d@.0,0.0,0.0)); 25 26 // Прикладываем силу, под воздействием которой 27 // начнет двигаться объект. 28 force theForce; 29 theForce.Force(vector_3dA.0,0.0,0.Of)); 30 theForce.ApplicationPoint(vector_3d@.0,0.0,-1.Of)); 31 theObject.Force(theForce); 32
242 Глава 9 33 theObject.MassA00); 34 35 return (true); 36 } В строках 4-9 листинга 9.2 функция Gamelnitialization () подготавливает матрицу отображения. В строках 13-16 подготавливается матрица проецирования. Поскольку позиция наблюдения и перспектива не изменяются от кадра к кадру, их не нужно пересчитывать, как это обычно происходит в примерах программ, поставляемых с SDK DirectX. В нашем примере матрицы отображения и проецирования создаются только один раз и после этого не изменяются. В строке 19 листинга 9.2 функция Gamelnitialization () загружает сетчатую модель, которая должна использоваться данным твердым телом. Это сетчатая модель тигра из SDK DirectX. Инициализация объекта класса d3d_rigid_body начинается в строке 21. В строках 21-24 задаются вращательные характеристики тигра. Вероятно, вас интересует, откуда взяты значения вращательной инерции в строке 23. Они вычислены - тигр рассматривался как цилиндр, и использовались формулы из рисунка 9.9. Обратите внимание, что в начальный момент работы программы тигр смотрит на вас - вдоль оси z. Если рассматривать тигра как цилиндр, то при вращении вокруг оси z используется формула I = MR2 / 2. Но при вращении вокруг осей х или у используется формула I = MR2 / 2 + ML2 /12. При расчетах я использовал длину 2 м (без учета хвоста) и массу 100 кг. Кроме того, в функции Gamelnitialization () также задается начальная сила, приводящая тигра в движение. Это делается в строках 26-29. Здесь есть один важный момент. Я создал для этой программы класс force для представления сил. Вместо сил, представляемых векторами и действующих на объекты, в которых они хранятся, теперь силы представляют собой самостоятельные объекты. Такое выделение необходимо, поскольку для описания силы во вращательной динамике нужно больше информации, чем в линейной. В линейной динамике силы всегда действуют на центр массы тела. Во вращательной динамике это не так. Работая с вращающимися твердыми телами, нужно учитывать силы, приложенные к разным точкам поверхностей этих тел. Почему силы могут прикладываться к разным точкам твердых тел? Представьте себе автомобильный симулятор. Врезаться в другой автомобиль можно с разных сторон - и не всегда с направления, параллельного какой-либо оси координат. Поэтому нужно учитывать возможности приложения сил, действующих в разных направлениях и приложенных к разным точкам поверхностей тел. Все эти рассуждения - просто попытка объяснить, почему в объекте класса force хранятся как вектор силы, так и вектор, указывающий, куда эта сила прикладывается. Определение класса force приведено в листинге 9.3.
Динамика твердых тел 243 Листинг 9.3. Класс force 1 class force 2 { 3 private: 4 vector_3d forceVector; 5 vector_3d forceLocation; 6 7 public: 8 9 // Вектор собственно силы. 10 void Force( 11 vector_3d theForce); 12 vector_3d Force(void); 13 14 // Вектор, указывающий точку приложения силы. 15 void ApplicationPoint( 16 vector_3d forceApplicationPoint); 17 vector_3d ApplicationPoint(void); 18 >; Этот класс прост. В нем содержатся всего два вектора и методы, позволяющие читать и записывать значения этих векторов. Если вы вернетесь к листингу 9.1, то увидите, что в классе d3d_rigid_body есть элемент-объект класса force. Кроме того, там есть методы для чтения и записи значения этого элемента. В функции GameInitialization(), код которой приведен в листинге 9.2, сила, действующая на тигра, задается вызовом метода Force () в строке 29. Сила прикладывается не к центру массы тигра, поэтому она заставляет тигра не только двигаться поступательно, но и вращаться. Если вы запустите программу, то увидите, что тигр медленно вращается, двигаясь по направлению к правому краю окна программы. Обновление объектов класса d3d_rigid_body Просчитывая каждый кадр, платформа вызывает функцию UpdateFra- me (), код которой приведен в листинге 9.4. Листинг 9.4. Функция UpdateFrame() 1 bool UpdateFrame() 2 { 3 static bool forceApplied = false; 4 5 // Если начальная сила уже была приложена... 6 if (forceApplied) 7 {
244 Глава 9 8 // Уменьшаем эту силу до 0. 9 force offCenterForce; 10 offCenterForce.Force(vector_3d@. 0, 0 .0,0 . Of)) ; 11 offCenterForce.ApplicationPoint(vector_3d@.0,0.0,0.0)); 12 theObject.Force(offCenterForce); 13 } 14 // В противном случае сила erne не была приложена... 15 else 16 { 17 // Приложим ее. 18 forceApplied=true; 19 ) 20 21 tbeObject.UpdateA); 22 return (true); 23 } Эта функция проверяет, была ли приложена к тигру начальная сила, заданная в функции Gamelnitialization (). Во время просчета первого кадра анимации сила еще не была приложена, поэтому функция UpdateFrame () прикладывает ее, вызывая метод d3d_rigid_ body: : Update (). После первого кадра сила уже была приложена. Мы не хотим, чтобы она прикладывалась снова, иначе тигр будет ускорять свое движение. Поэтому в строках 9-12 листинга 9.4 функция UpdateFrame () уменьшает силу до 0. Посмотрим на метод d3d_rigid_body: : Update (). Именно в нем проводятся все расчеты, связанные с моделированием физики. Листинг 9.5. Метод d3d_rigid_body::Update() 1 bool d3d_rigid_body::Update( 2 scalar changeInTime) 3 { 4 // 5 // Начинаем с расчета линейной динамики. 6 // 7 8 // Находим линейное ускорение. 9 // а = F/m 10 assert(mass!=0); 11 linearAcceleration = sumForces.Force()/mass,- 12 13 // Находим линейную скорость. 14 linearVelocity += linearAcceleration * changelnTime; 15 16 // Находим новое местоположение центра массы. 17 centerOfMassLocation += linearVelocity * changelnTime; 18
Динамика твердых тел 245 19 // 20 // Линейная динамика просчитана. 21 // 22 23 // Создаем матрицу перемещения. 24 D3DXMATRIX total-Translation; 25 D3DXMatrixTranslation( 26 StotalTranslation, 27 centerOfMassLocation.X{), 28 centerOfMassLocation.Y(), 29 centerOfMassLocation.Z()); 30 31 // 32 // Начинаем расчет вращательной динамики. 33 // 34 35 //По известной силе находим вращающий момент. 36 torque = 37 sumForces.ApplicationPoint().Cross(sumForces.Force()); 38 39 /* По вращающему моменту и инерции вычисляем 40 угловое ускорение.*/ 41 angularAcceleration.X{ 42 torque.X()/rotationallnertia.X()); 43 angularAcceleration.У( 44 torque.Y()/rotationallnertia.Y<)); 45 angularAcceleration.Z( 46 torque.Z()/rotationallnertia.Z()); 47 48 /* Изменяем угловую скорость согласно угловому ускорению. */ 49 angularVelocity += angularAcceleration * changelnTime; 50 51 // 52 // Используем угловое ускорение, чтобы найти углы вращения. 53 // 54 currentOrientation.XAngle( 55 currentOrientation.XAngle() + 56 angularVelocity.X() * changelnTime); 57 currentOrientation.YAngle( 58 currentOrientation.YAngle() + 59 angularVelocity.Y() * changelnTime); 60 currentOrientation.ZAngle( 61 currentOrientation.ZAngle() + 62 angularVelocity.Z() * changelnTime); 63 64 // 65 // Завершили расчет вращательной динамики. 66 //
246 Глава 9 67 68 // Создаем матрицы вращения для каждой оси. 69 D3DXMATRIX rotationX, rotationY, rotationZ; 70 D3DXMatrixRotationX(SrotationX,currentOrientation.XAngle()) 71 D3DXMatrixRotationY(SrotationY,currentOrientation.YAngle()) 72 D3DXMatrixRotationZ(SrotationZ,currentOrientation.ZAngle()) 73 74 D3DXMATRIX totalRotations; 75 76 // Перемножаем их, чтобы получить глобальную матрицу. 77 D3DXMatrixMultiply( 78 StotalRotations, 89 SrotationX, 80 SrotationY); 81 D3DXMatrixMultiply( 82 StotalRotations, 83 StotalRotations, 84 SrotationZ); 85 86 /* Объединяем матрицы вращения и перемещения 87 в глобальную матрицу. */ 88 D3DXMatrixMultiply( 89 SworldMatrix, 90 StotalRotations, 91 StotalTranslation); 92 93 return(true); 94 } Как видите, львиную долю расчетов выполняет именно этот метод. Первое, что он делает - находит линейное перемещение объекта по приложенной к нему силе. Из приложенной силы и массы объекта метод Update () находит ускорение. В строке 14 листинга 9.5 метод находит изменение скорости. По этому изменению скорости находится вектор смещения для центра массы тела. Этот вектор включается в матрицу перемещения в строках 24-29. Пока твердое тело воспринималось как материальная точка. Начиная со строки 36, метод Update () начинает использовать отличия между материальными точками и твердыми телами. В строках 36-37 по формуле т = г X F вычисляется вращающий момент объекта. По вращающему моменту и инерции вращения метод находит линейное ускорение в строках 41-46. Затем по угловой скорости вычисляются углы вращения по осям х, у и z. Когда эти углы найдены, создаются матрицы вращения (строки 69-72). Работа метода Update () почти закончена. Но прежде чем он завершается, он совмещает все три матрицы вращения в одну в строках 77-84. Затем он объединяет матрицу вращения и матрицу перемещения в глобальную матрицу в строках 88-91. На этом выполнение метода Update () заканчивается.
Динамика твердых тел 247 Рендеринг объекта класса d3d_rigid_body Как и обновление объекта класса d3d_rigid_body, рендеринг выполняется двумя функциями. Первая из этих функций — RenderFrame (), вызываемая платформой. Код этой функции приведен в листинге 9.6. Листинг 9.6. Функция RenderFrame() 1 bool RenderFrame() 2 { 3 // Задаем матрицу отображения. 4 theApp.D3DRenderingDevice()->SetTransform( 5 D3DTS_VIEW, 6 &theApp.ViewMatrix()) ; 7 8 // Задаем матрицу проецирования 9 theApp.D3DRenderingDevice()->SetTransform( 10 D3DTS_PROJECTION, 11 &theApp.ProjectionMatrix()); 12 13 // Выполняем рендеринг объекта. 14 theObject.Render(); 15 return (true); 16J Как видно из листинга 9.6, функция RenderFrame () заново задает матрицы отображения и проецирования при рендеринге каждого кадра. Вы вправе спросить, необходимо ли это. Учитывая, что позиция наблюдения и видимая область остаются неизменными, ответ - нет. Для этого примера можно перенести вызовы функций в строках 4-11 в функцию GameInitialization(). Тогда почему же они расположены здесь? Чтобы проиллюстрировать один момент. В большинстве игр матрицу отображения нужно обновлять при просчете каждого кадра, поскольку камера непрерывно движется в мире игры. Матрица проецирования тоже непрерывно изменяется. Если в вашей игре будет также, то обе матрицы нужно задавать заново при рендеринге каждого нового кадра. Их можно задавать в функции UpdateFra- me (), но это должно быть последним действием функции. Если программе нужно изменить их, это лучше делать здесь, в функции RenderFrame (). Рендеринг тигра выполняет вызываемый функцией RenderFrame () метод d3d_rigid_body: : Render (), код которого приведен в листинге 9.7. Как и для материальных точек, метод Render () для твердых тел гораздо проще, чем метод Update (). Метод Render () сохраняет ранее использовавшуюся глобальную матрицу и задает свою. Затем выполняется рендеринг сетчатой модели в заданной позиции, а после этого восстанавливается ранее сохраненная глобальная матрица.
248 Глава 9 Листинг 9.7. Рендеринг твердого тела 1 bool d3d_rigid_body: :Render (void) 2 { 3 // Сохраняем матрицу глобального преобразования. 4 D3DXMATRIX saveWorldMatrix; 5 theApp.D3DRenderingDevice{)->GetTransform( 6 D3DTSJW0RLD, 7 SsaveWorldMatrix); 8 9 // Применяем эту матрицу к объекту. 10 theApp.D3DRenderingDevice()->SetTransform( 11 D3DTS_WORLD,SworldMatrix); 12 13 // Выполняем рендеринг объекта после преобразований. 14 bool renderedOK=objectMesh.Render(); 15 16 // Восстанавливаем матрицу глобального преобразования. 17 theApp.D3DRenderingDevice{)->SetTransform( 18 D3DTS_WORLD, 19 SsaveWorldMatrix); 20 21 return (renderedOK); 22 ) Итоги В этой главе было много и математики, и программирования. Те инструменты, которые у нас есть теперь, позволят нам моделировать почти любой объект в играх. Практически все можно представить в виде материальной точки, твердого тела, набора материальных точек или набора твердых тел. Как вы вскоре увидите, наши возможности стали очень обширными. Но, прежде чем мы сможем использовать эти классы в играх, нужно разобраться со столкновениями твердых тел. Этому посвящена следующая глава.
Глава 10 Столкновения твердых тел Моделирование столкновений твердых тел сводится к усовершенствованию алгоритмов, использовавшихся при моделировании столкновений материальных точек. Как и для материальных точек, здесь моделирование столкновений делится на две задачи: обнаружение столкновений и реагирование на столкновения. Обнаружение столкновений Какие методики обнаружения столкновений лучше всего использовать, зависит от того, какую игру вы пишете. Для многих игр достаточно использовать грубые приближенные методики. Это особенно верно для аркад. Однако если вы пишете сложные имитаторы, в которых, например, нужно моделировать идущего человека, то грубые приближения не подойдут. Поэтому для написания ЗБ-игр нужно знать обширный набор различных методик - от самых простых до наиболее сложных. Грубые приближения Основное преимущество грубых приближенных методов обнаружения столкновений в играх - быстрота работы таких методов. Кроме того, их сравнительно просто реализовывать в коде. Из этой категории методов наиболее распространены методы ограничивающих сфер, цилиндров и блоков прямоугольной формы. В главе 8 рассматривались алгоритмы обнаружения столкновений между материальными точками, основанные на этих методах. Если вы используете в своей игре такие методы обнаружения столкновений, это, в общем, означает, что вы аппроксимируете твердые тела как материальные точки в рамках поступательного движения. Это во многих случаях хорошее приближение. Почти во всех 3D-играх линейные силы, воздействующие на твердые тела, можно моделировать как воздействующие на материальные точки, без потерь реалистичности. Если это так и в вашей игре, то, вероятно, обнаруживать столкновения между объектами в ней можно с помощью ограничивающих сфер, цилиндров и блоков прямоугольной формы.
250 Глава 10 Выбор сфер, цилиндров или блоков прямоугольной формы для обнаружения столкновений в конечном итоге зависит от формы объектов, которые вы моделируете. Предположим, например, что вы пишете игру, в которой используются ракеты и бомбы. Для бомб можно использовать сферы, а для ракет - цилиндры. С точки зрения поступательного движения можно рассматривать все эти объекты как материальные точки. Их форма довольно проста, и использование ограничивающих сфер и цилиндров будет давать хорошие результаты. Если моделируются объекты более сложной формы, использовать для обнаружения столкновений грубые приближенные методы бывает неудобно. Например, на рисунке 10.1 показана модель тигра, которую мы использовали в примерах из предыдущих глав. Вообще-то тигры - не слишком правдоподобный пример твердых тел, но можно посчитать, что это статуя тигра. Как продемонстрирует пример в этой главе, тигры не слишком хорошо размещаются в сферах. Цилиндры и прямоугольные блоки дадут несколько лучший результат, но и они недостаточно хороши. Рис. 10.1. Модель тигра в прямоугольном ограничивающем блоке Представьте себе две такие модели, вращающиеся в пространстве. Пока забудем, что тигры не могут находиться в космосе. Используя простые ограничивающие формы, например, прямоугольные блоки, как на рисунке 10.1, игра будет обнаруживать несуществующие столкновения между объектами. Как это может случиться, показывает рисунок 10.2. Как видно из рисунка 10.2, ограничивающие блоки могут пересекаться, но столкновения между самими моделями при этом нет. Тигр - объект слишком сложной формы, чтобы вокруг него можно было точно описать простую форму — сферу, цилиндр или прямоугольный блок.
Столкновения твердых тел 251 Рис. 10.2. Недостатки использования прямоугольных ограничивающих блоков Улучшенные методы обнаружения столкновений Как можно улучшить простейшие методы обнаружения столкновений, не слишком увеличивая уровень загрузки процессора? Было сделано множество попыток ответить на этот вопрос - более или менее успешных. Важно не забывать, что обнаружение столкновений — задача не физики, а геометрии и программирования. Поскольку эта книга в основном о физическом моделировании, в ней есть только краткий обзор улучшенных методов обнаружения столкновений.
252 Глава 10 ДЕРЕВЬЯ ОГРАНИЧИВАЮЩИХ ПОВЕРХНОСТЕЙ Один из наиболее универсальных и эффективных улучшенных методов обнаружения столкновений основан на применении наборов ограничивающих поверхностей, хранимых в виде массива или дерева. Давайте еще раз посмотрим на тигра, чтобы разобраться, как может работать такой метод. На рисунке 10.3 показан тигр, ограниченный несколькими прямоугольниками, а не одним, как раньше. Рис. 10.3. Более точный набор прямоугольников, ограничивающих тигра На рисунке 10.3 используется шесть ограничивающих блоков прямоугольной формы. Во-первых, весь тигр целиком заключен в ограничивающий блок, как и на рисунке 10.1. Этот блок выделен серой рамкой и обозначен цифрой 6. Столкновение с этим блоком означает, что столкновение с тигром возможно, хотя и не обязательно. Возможно, имел место случай, аналогичный показанному на рисунке 10.2. Программа проверяет, действительно ли произошло столкновение, перебирая остальные ограничивающие блоки. На рисунке 10.3 это ограничивающие блоки для головы, передних лап, туловища, задних лап и хвоста тигра. Чтобы блоки было легче различить, они выделены черными рамками и пронумерованы. Если происходит столкновение с внешним ограничивающим блоком, но не с внутренними, значит, точка столкновения находится в пустом пространстве внутри блока б. Это на самом деле не столкновение, и программа не должна на него реагировать. Возможно, вы спросите, зачем же вообще нужен внешний ограничивающий блок. Единственная причина его использования - повышение эффективности. Если нет столкновения с внешним ограничивающим блоком, нет нужды проверять внутренние блоки - столкновений с ними
Столкновения твердых тел 253 гарантированно нет. Использование внешнего блока и внутренних позволяет достичь высокой скорости работы, если столкновений нет, и высокой точности, если они есть. Замечание Вероятно, у вас возникнет искушение избавиться от внешнего ограничивающего блока в этом примере, чтобы сэкономить немного памяти. Я не рекомендую вам это делать. Это было необходимо лет двадцать назад, когда 256 кб было большим объемом памяти. Однако в современных компьютерах памяти достаточно, чтобы разработчикам программ не нужно было трястись над каждым байтом. Основной ограничивающий фактор в большинстве игр - скорость вычислений, а не объем памяти, поэтому в общем случае стоит потратить немного больше памяти, чтобы выиграть в скорости. А обязательно ли всем ограничивающим поверхностям быть одинаковой формы? Проще говоря, можно ли использовать сферу для ограничения головы, цилиндры - для лап, и прямоугольники - для туловища и хвоста? Конечно, можно. Это очень просто сделать, используя возможности наследования в языке C++. Чтобы использовать ограничивающие поверхности разных типов, можно создать базовый класс, от которого порождать все классы разных ограничивающих поверхностей. Назовем этот базовый класс bounding_volume. От него можно породить классы bounding_sphere для сфер, bounding_rectangle для прямоугольных блоков и bounding_cylinder для цилиндров. Все эти классы будут производными от bounding_volume, и их можно будет хранить в общих структурах данных. Если уж мы упомянули структуры данных - как лучше всего хранить наши наборы ограничивающих поверхностей? Ответ: как хотите. Их можно хранить в статическом массиве объектов или массиве указателей на динамически создаваемые объекты. Часто их хранят в древовидных структурах, позволяющих быстро выполнять поиск. Например, если потенциальное столкновение происходит около переднего края модели тигра из предыдущих примеров, можно проверить значения координат х и у в месте столкновения. Если начало системы координат совпадает с центром масс тигра, то значение х < 0 указывает, что столкновение, вероятно, произойдет возле головы. Поэтому сначала нужно проверить, есть ли столкновение с блоком 1, затем - с блоком 2, затем - с блоком 3. Поскольку х < 0, то проверять блоки 4 и 5 вообще незачем - если столкновения не было в блоках 1-3, значит, его не было вообще. Предупреждение Такой подход работает, только если преобразовать координаты потенциального столкновения из глобальной системы координат в локальную систему координат объекта.
254 Глава 10 ВЫРОВНЕННЫЕ ПО ОСЯМ ОГРАНИЧИВАЮЩИЕ БЛОКИ И ОРИЕНТИРОВАННЫЕ ОГРАНИЧИВАЮЩИЕ БЛОКИ Если ограничивающие блоки достаточно точны для вашей игры, они могут работать очень быстро. В играх чаще всего используются ограничивающие блоки двух типов. Первый - это выровненные по осям ограничивающие блоки (axis-aligned bounding boxes - ААВВ). Чаще всего ААВВ хранятся в специальной структуре данных, называемой октальным деревом (octree). Преимущество ААВВ в том, что их ребра всегда параллельны осям х, у, z глобальной системы координат. Поэтому, проверяя есть ли столкновение, программа просто проверяет координаты х, у, z точки столкновения - попадают ли они в границы ААВВ? Такие проверки выполняются быстро. Что такое октальные деревья? Во всех обсуждениях методов обнаружения столкновений всегда упоминаются октальные деревья (octree). Эти деревья отличаются от других типов деревьев. Например, в бинарных деревьях у каждого узла может быть один левый и один правый дочерний узел, поэтому такое дерево легко изобразить на плоскости - это фактически 20-структуры. Октальные деревья - это ЗЭ-структуры. Представьте себе, что мы разрезали лазером ЗО-фигуру на прямоугольные блоки. Предположим, что все блоки остаются на своих местах. Собственно говоря, мы поделили пространство, в котором находилась фигура, на прямоугольные блоки. А теперь проиндексируем блоки так, чтобы они образовали древовидную структуру. В дереве будет указатель на каждый блок. Собственно говоря, дерево предоставит способ быстро переходить от одного блока к другому, соседнему блоку в 3D. Именно для этого предназначены октальные деревья. Недостаток ААВВ - они остаются привязанными к осям глобальной системы координат, даже если ограничиваемый ими объект поворачивается. При этом точность обнаружения столкновений будет изменяться, и могут появиться ложные обнаружения. Поэтому ААВВ лучше всего применять для объектов, ориентация которых не изменяется со временем и совпадает с осями глобальной системы координат — например, зданий. Другой тип ограничивающих блоков - ориентированные (oriented bounding boxes - ОВВ). Как и в ААВВ, в ОВВ помещаются части ограничиваемого твердого тела. ОВВ размещаются так, чтобы как можно теснее ограничить эти части. Однако в отличие от ААВВ, ОВВ размещаются в локальной системе координат тела и вращаются, перемещаются и масштабируются вместе с этим телом. Поскольку ОВВ определены в локальной системе координат, программе приходится пересчитывать координаты точек, прежде чем проверить, есть ли столкновение. Это приводит к появлению дополнительных накладных расходов по сравнению с использованием ААВВ, но эти расходы
Столкновения твердых тел 255 невелики. Кроме того, ОВВ куда лучше описывают объекты, которые не выровнены вдоль осей глобальной системы координат. Как и ААВВ, ОВВ обычно хранятся в октальных деревьях - это позволяет быстрее определять, произошло ли столкновение. Но не считайте использование октальных деревьев непременным. Если твердые тела несложны по форме, октальные деревья использовать незачем — это приведет только к замедлению работы программ. Если вы используете только простые твердые тела, используйте более простые структуры данных. Реакция на столкновения Реакция на столкновения в программе - это проблема физического моделирования. Чтобы получить точную модель сложных физических объектов, программа должна моделировать и поступательную, и вращательную динамику. Кроме того, для каждого твердого тела нужно хранить информацию, позволяющую просчитывать его реакцию на столкновения. Это, в частности, объем тела и его коэффициент восстановления. Замечание Напомню, что коэффициент восстановления, который рассматривался в главе 8, - это мера эластичности объекта. В играх нужно моделировать силы, действующие на твердые тела при столкновениях. Эти силы должны учитываться при вычислении поступательных и вращательных реакций на столкновения. Линейная реакция на столкновения Линейная реакция твердого тела на столкновение определяется теми же уравнениями, что и реакция материальной точки. Почему? Потому что в этом случае мы можем заменить твердое тело равной ему по массе материальной точкой, расположенной в его центре массы. А это значит, что любое твердое тело в игре можно считать материальной точкой, расположенной в его центре массы. Поэтому мы уже знаем, как рассчитать линейную реакцию на столкновение. Мы это делали в главе 8. Все формулы, выведенные там, можно использовать здесь. В главе 8 мы убедились, что при столкновениях возникают силы. Эти силы являются следствием движения тел, участвующих в столкновении. Это значительные силы, действующие в течение коротких промежутков времени. Такие силы в физике называются импульсными силами (impulse force). При столкновениях материальных точек импульсные силы действуют на эти точки. Но сталкивающиеся твердые тела могут соприкасаться в нескольких точках одновременно, как показано на рисунке 10.4.
256 Глава 10 При столкновении твердых тел 1 и 2, изображенных на рисунке 10.4, в точках их соприкосновения возникают силы. Точки соприкосновения обозначены Р и Q. На рисунке двигается только тело 1, поэтому только оно вносит в столкновение силы. Однако эти силы действуют на оба участвующих в столкновении тела. Результаты воздействия этих сил определяются уже знакомым нам выражением F = та. Рис. 10.4. Столкновение двух твердых тел Вопрос заключается в следующем: как смоделировать эти силы во всех точках соприкосновения при столкновении? Ответ - это незачем делать. Чтобы моделировать линейное движение, достаточно считать твердые тела материальными точками. Поэтому все силы, возникающие при столкновении, прикладываются к материальным точкам, расположенным в центрах масс тел. Вместо того, чтобы разбираться с силами, действующими в каждой точке соприкосновения, достаточно просуммировать эти силы и приложить их к центрам масс участвующих в столкновении твердых тел. Не забывайте, что общие силы, возникающие в столкновении, одинаковы для обоих твердых тел. Но сила, действующая на тело 1, равна FI? a действующая на тело 2 равна -Fj. Угловая реакция на столкновение В главе 9 мы вывели уравнения, описывающие вращательное движение твердого тела. Эти уравнения можно применять и для моделирования столкновений. Но, работая с угловыми силами, мы не можем считать твердое тело материальной точкой. Эти силы действуют не на центр массы, а на точку соприкосновения. Посмотрите еще раз на рисунке 10.4. Там изображено столкновение двух твердых тел. Однако точки соприкосновения две. Так как же быть? Ответ - изворачиваться. Большинство объектов в играх весьма правильной и симметричной формы. Безусловно, чем более реалистичными становятся игры, тем менее правильна и симметрична форма объектов в них. Однако в большинстве
Столкновения твердых тел 257 случаев это неважно. Обычно можно сделать столкновения правдоподобными, просчитывая столкновения только для одной точки. А если все выглядит правдоподобно, значит, все нормально, В столкновении, изображенном на рисунке 10.4, можно извернуться так, как показано на рисунке 10.5. Суть фокуса, показанного на рисунке 10.5. в нахождении точки центра столкновения. Точка R находится посередине между точками Р и Q, и, чтобы просчитать угловую реакцию на столкновение, довольно будет приложить силу к точке R обоих тел. Несмотря на то, что на самом деле точка R не принадлежит телу 1, можно считать, что она принадлежит ему, и приложить к ней импульсную силу. Результаты такого фокуса будут достаточно точными для практически всех игр, и такой прием довольно быстро просчитывается. Рис. 10.5. Упрощение столкновения Можно применить тот же фокус, моделируя столкновения плоскостей, а не точек. Предположим, к примеру, что автомобиль врезается в правое переднее крыло другого автомобиля под углом 90°. В столкновении участвуют вся передняя часть первого автомобиля и большая часть правого переднего крыла второго автомобиля. Чтобы быстро и достаточно точно смоделировать угловую реакцию, ограничимся точкой, в которой центр переднего бампера первой машины соприкасается со второй машиной, как показано на рисунке 10.6. Точка Pcenter на рисунке 10.6 показывает центр столкновения. Если приложить силы к этой точке, результаты будут выглядеть правильными. Совмещение линейной и угловой реакции на столкновение В столкновениях по прямым, не проходящим через центры масс сталкивающихся тел, проявляются и линейная, и угловая реакция. Поэтому нам нужна зависимость, позволяющая определить импульсную силу с учетом как линейных, так и угловых компонентов. Чтобы найти эту зависимость, начнем со второго закона Ньютона.
258 Глава 10 Рис. 10.6. Выбор точки центра столкновения Применив эту формулу к импульсной силе, мы получим: Fr = mat Здесь Fj - импульсная сила, m - масса, aat- ускорение при столкновении. Ускорение можно записать как: ах = vf - Vi В этом уравнении vf и v4 есть скорости тела после столкновения и до столкновения, соответственно. К несчастью, нам неизвестна vf. Поэтому нам нужно еще одно уравнение. Вспомните главу 8. Выражение для коэффициента восстановления выглядело так: с_ -(Vlf~V2f) VU-V2. Теперь у нас есть три вопроса, которые помогут нам найти ответ. Почему три? Потому что магнитуда импульсной силы равна для обоих тел, противоположны только направления действия этой силы. Поэтому можно записать: FI=ml(vlf-vli) -F, = m2(v2f - v2i) с_ -(Vlf~V2f) Vh-V2i
Столкновения твердых тел 259 Замечание Мы обозначаем импульсные силы F,, однако, в большинстве книг по физике используется обозначение J. Теперь у нас есть три уравнения и три неизвестных (Fj, vlf и v2f), поэтому можно найти vlf и v2f и подставить результаты в выражение для е. Получим: (( е = ш, +vn ггь "+V2i Vli-V2i Часть этого выражения - выражение v1A - v2i, обозначающее скорость сближения тел до столкновения. Мы упростим себе дальнейшую работу, если запишем vr = vn-v2i Выполнив замену в предыдущем выражении, получим: (( е = -+V, т. \\ ггь '+V2i Теперь у нас есть выражение для линейной импульсной силы, в котором участвуют массы, начальные скорости тел и коэффициент восстановления. Следующий шаг - добавить угловую реакцию на столкновение. В главе 9 говорилось, что скорость точки Р, находящейся на расстоянии г от центра массы, выражается формулой vp = со X г Это уравнение можно использовать как до столкновения, так и после. Если выбрать точку Р точкой столкновения, то можно найти ее полную скорость после столкновения: V = V р cm + (... X Г) В этом уравнении мы суммируем вращательную скорость точки и линейную скорость центра масс, чтобы найти конечную скорость точки столкновения Р. Применение этого уравнения к каждому твердому телу, участвующему в столкновении, даст нам такие выражения: ( Kfcml Л + V т, п + (-, *ij) rfcm2 Ш-, + V2i + (-2 ХГ2)
260 Глава 10 Теперь у нас есть еще две неизвестные. Это vfcml и vfcm2 ~ скорости центров масс твердых тел 1 и 2 после столкновения. Поскольку у нас появились две новые неизвестные, нам нужны еще два уравнения. Их можно получить из формул для нахождения вращающего момента: т = г х F т = la = I(...f - ...j) Приравняв эти две формулы друг к другу, мы получим: г xF = I(...f-...i) Применив это равенство к каждому из твердых тел, участвующих в столкновении, мы получим: гх X Fx = l!(...lf - ...и) г2 X (-FT) = I2(...2f - ...2i) Выполнив подстановки и преобразовав результат, мы получим: Впечатляет, не правда ли? Пришлось поднапрячься, но мы получили -vr(e+l) Fi = 1 + 1 + n' m пь ^(rjxn) I LV Л ) xrl _ + n» у -V (r2Xn) I, Л XI% формулу для вычисления импульсной силы, учитывающую и линейную, и угловую реакцию тел на столкновение. Обратите внимание - вектор п в этой формуле есть единичный нормальный вектор в точке Р. Вот и все, что нам понадобится. Если мы можем задать импульсную силу и точку, в которой она возникает, мы можем передать эту информацию классу d3d_rigid_body, представленному в главе 9. При этом можно просчитать поведение каждого тела в столкновении. Перейдем к коду. Обновление платформы физического моделирования В главе 9 мы рассмотрели класс d3d_rigid_body, моделирующий как вращательную, так и линейную динамику твердых тел. В этой главе мы расширим возможности данного класса и модифицируем платформу физического моделирования, частью которой он является. Начиная с этой главы, классы d3d_mesh и d3d_rigid_body имеют гораздо больше отношения к физическому моделированию, чем к Direct3D. Поэтому я переименовал их просто в mesh и rigid_body. Кроме того, теперь платформа физического моделирования будет состоять из трех библиотек. Первая - это математическая библиотека, созданная в главе 2 «Имитация ЗБ-графики с помощью DirectX» и главе 3 «Математические инструменты». Вторая - это графическая библиотека, выполняющая подготовку Direct3D к запуску. В этой главе во второй библиотеке будет содержаться только класс d3d_app.
Столкновения твердых тел 261 Третья библиотека платформы содержит классы моделирования физических элементов - сил, сетчатых моделей, твердых тел и столкновений. Вскоре вы познакомитесь с тем, как используется эта библиотека на практике. Замечание Иногда полезно знать, почему авторы программ принимали те или иные решения. В нашем случае решения приняты исходя из личных предпочтений и соображений удобства. Например, можно поспорить, должен ли класс mesh принадлежать к физической или графической библиотекам, и нужно ли было его переименовывать. Альтернативная позиция по этому вопросу вполне логична и имеет право на существование. С моей точки зрения, класс mesh было удобнее и логичнее поместить в физическую, а не в графическую библиотеку. Начиная с главы 9, объявление каждого класса помещено в отдельный заголовочный файл. Например, определение класса force теперь находится в файле force.h. Заглянув в папку Source\ChapterlO\ TigerToss на поставляющемся с книгой компакт-диске, вы увидите в ней множество заголовочных файлов. К счастью, вам не нужно беспокоиться о том, какие из них включать в проекты и в каком порядке это делать. Просто включите файл PMFramework. h во все файлы . срр проекта. ПРИВЕДЕНИЕ ОБЪЕКТОВ В ДВИЖЕНИЕ Пример программы в этой главе называется Tiger Toss. Эта программа создает три твердых тела в форме тигров. Затем эти тела начинают двигаться, совмещая и поступательные, и вращательные движения. При столкновениях этих тел появляются и линейные, и вращательные реакции. Замечание Настоящие тигры в этих столкновениях не участвовали. Чтобы разобраться, как работает программа, посмотрим сначала на функции из файла TigerToss. срр. Он находится в папке Source\Chap- terlO\TigerToss на поставляющемся с книгой компакт-диске. Листинг 10.1. Инициализация объектов программы 1 bool Gamelnitialization() 2 { 3 // Создаем матрицу отображения - как в предыдущих примерах. 4 D3DXVECTOR3 eyePoint@.Of,3.Of,-10.Of); 5 D3DXVECTOR3 lookatPoint@.Of,0.Of,0.Of); 6 D3DXVECTOR3 upDirection@.Of,1.Of,0.Of); 7 D3DXMATRIXA16 tempViewMatrix;
262 Глава 10 8 D3DXMatrixLookAtLH( 9 &tempViewMatrix,&eyePoint,&lookatPoint,6upDirection); 10 theApp.ViewMatrix(tempViewMatrix); 11 12 // Создаем матрицу проецирования. 13 D3DXMATRIXA16 projectionMatrix; 14 D3DXMatrixPerspectiveFovLH( 15 &projectionMatrix,D3DX_PI/4,1.0£,1.0f,100.Of); 16 theApp.ProjectionMatrix(projectionMatrix); 17 18 vector_3d tempVector@.0,0.0,0.0); 19 force theForce; 20 21 /* Загружаем сетчатую модель титра. 22 Не забудьте: эта модель поставляется вместе с SDK DirectX. 23 Ее нужно скопировать из папки 24 <SDKDIR>\Samples\C++\Direct3D\Tutorials\Tut06_Meshes, 25 где <SDKDIR> - полный путь к папке, в которую установлен 26 SDK DirectX. Скопируйте файлы tiger,x и tiger.bmp 27 в папку проекта.*/ 28 allTigers[0].LoadMesh("tiger.x"); 29 30 // Задаем начальное местоположение первого тигра. 31 tempVector.SetXYZ(-3.Of,0.0,0.0); 32 allTigers[0].Location(tempVector); 33 34 // Задаем вращательную инерцию первого тигра. 35 tempVector.SetXYZC9.6f,39.6f,12.5f); 36 allTigers[0].Rotationallnertia(tempVector); 37 38 // Задаем вектор силы, определяющий линейное движение тигра. 39 tempVector.SetXYZA.0,0.0,0.Of); 40 theForce.Force(tempVector); 41 42 // Задаем точку, к которой приложена сила. 43 tempVector.SetXYZ@.0,0.0,-1.Of) ; 44 theForce.ApplicationPoint (tempVectoi?) ; 45 46 // Сохраняем силу в объекте класса irigid_body (тигре). 47 allTigers[0].Force(theForce); 48 49 // Задаем массу тигра. 50 allTigers[0].Mass A00) ; 51 52 // Задаем ограничивающую сферу тигра. 53 ' allTigers[0].BoundingSphereRadius@.75f); 54 55 // Задаем упругость тигра. 56 allTigers[0].CoefficientOfRestitution@.9f);
Столкновения твердых тел 263 57 58 // Копируем характеристики первого тигра во всех остальных. 59 allTigers[2]=allTigers[l]=allTigers[0]; 60 61 /* Задаем другое начальное местоположение для второго тигра. */ 62 tempVector.SetXYZ{0.0,3.Of,0.0); 63 allTigers[l].Location(tempVector); 64 65 // Прикладываем другую силу. 66 tempVector.SetXYZ(-l.Of,-1.0f,0.0); 67 theForce.Force(tempVector); 68 tempVector.SetXYZ@.0,-1.Of,-1.Of); 69 theForce.ApplicationPoint(tempVector); 70 allTigers[l].Force(theForce); 71 72 // Делаем второго тигра полностью упругим. 73 allTigers[1].CoefficientOfRestitutionA.Of); 74 75 /* Задаем другое начальное местоположение для третьего тигра. */ 76 tempVector.SetXYZ@.0,-3.Of,0.0); 77 allTigers[2].Location(tempVector); 78 79 // Прикладываем другую силу. 80 tempVector.SetXYZ@.0,2.0,0.0); 81 theForce.Forсе(tempVector); 82 tempVector.SetXYZA.Of,-1.0f ,0.0) ; 83 theForce.ApplicationPoint(tempVector); 84 allTigers[2].Force(theForce); 85 86 // Делаем третьего тигра малоупругим. 87 allTigers[2].CoefficientOfRestitution@.5f); 88 89 return (true); 90} Функция Gamelnitialization () начинается так же, как и в главе 9. В строках 4-10 создается матрица отображения, а в строках 13-16 — матрица проецирования. В строках 18-19 объявляются переменные, которые понадобятся позже. В строке 28 загружается сетчатая модель тигра. Обычно, если в игре множество объектов используют одну и ту же сетчатую модель, игра загружает эту модель только один раз. И, если при этом объекты, использующие модель, никак ее не изменяют, то такой подход позволяет очень заметно ускорить работу и сэкономить память. В программе Tiger Toss модель тигра загружается однажды и используется для всех трех тигров. В строках 31-32 листинга 10.1 функция Gamelnitialization () задает начальное местоположение первого тигра. Объекты-тигры хранятся
264 Глава 10 в массиве, объявленном в начале файла TigerToss. cpp. Вот как выглядит объявление этого массива: #define TOTALJEIGERS 3 rigid_body allTigers[TOTAL_TIGERS]; Переменная allTigers - это просто массив объектов типа ri- gid_body. Константа TOTAL_TIGERS объявлена, чтобы было проще перебирать объекты в массиве. В строках 35-36 задаются значения вращательной инерции тигра по осям х, у и z. Я использовал формулы из рисунка 9.9 в главе 9, чтобы вычислить эти значения. Тигр рассматривался как цилиндр массой 100 кг B20 фунтов) и длиной 2 м F футов 6 дюймов). Замечание Это, вероятно, маловато для взрослого тигра, но эти числа упрощают расчеты по формулам. В играх приходится анализировать каждое твердое тело и прибегать к упрощениям, чтобы вычислить вращающие моменты. Нужно просто посмотреть на объект и решить, в каких его частях сосредоточена основная часть массы, а затем найти на рисунке 9.9 формы, подходящие для описания этих частей. По формулам для этих форм можно вычислить их моменты, а затем с помощью теоремы Гюйгенса найти общий момент для всего твердого тела. Задав моменты, функция Gamelnitialization () задает силу, действующую на первого тигра. Это делается в строках 39-47. Затем задаются масса тигра, радиус ограничивающей сферы и коэффициент восстановления. У этого тигра он равен 0.9. В строке 59 листинга 10.1 вся информация из первого объекта копируется в два других объекта. Это позволяет не инициализировать все одинаковые свойства каждого объекта по отдельности. В строках 62-73 задаются характеристики второго тигра. Обратите внимание, что второй тигр полностью эластичен - коэффициент восстановления у него равен 1.0. Характеристики последнего тигра задаются в строках 76-87. У этого тигра коэффициент восстановления равен всего лишь 0.5 - он не слишком упруг по сравнению с двумя другими. ОБНАРУЖЕНИЕ И ОБРАБОТКА СТОЛКНОВЕНИЙ В программе Tiger Toss столкновения обрабатываются функциями UpdateFrame () и HandleOverlappingO, расположенными в файле TigerToss .cpp. Но прежде чем мы разберемся с этими функциями, посмотрим на некоторые части кода, от которых зависят эти функции (см. листинг 10.2).
Столкновения твердых тел 265 Листинг 10.2. Новая версия класса rigid_body 1 class rigid_body 2 { 3 private: 4 mesh objectMesh; 5 6 // Физические характеристики и характеристики 7 // поступательного движения. 8 scalar mass; 9 vector_3d centerOfMassLocation; 10 vector_3d linearVelocity; 11 vector_3d linearAcceleration; 12 force sumForces; 13 14 // Характеристики вращательного движения 15 angle_set_3d currentOrientation; 16 vector_3d angularVelocity; 17 vector_3d angularAcceleration; 18 vector_3d rotationalInertia; 19 vector_3d torque; 20 21 // Характеристики для моделирования столкновений. 22 scalar coefficientOfRestitution; 23 scalar boundingSphereRadius; 24 25 D3DXMATRIX worldMatrix; 26 27 public: 28 rigid_body(void); 29 30 bool LoadMesh( 31 std::string meshFileName); 32 33 void Mass( 34 scalar massValue); 35 scalar Mass(void) ; 36 37 void Location( 38 vector_3d locationCenterOfMass); 39 vector_3d Location(void); 40 41 void LinearVelocity( 42 vector_3d newVelocity); 43 vector_3d LinearVelocity(void); 44 45 void LinearAcceleration( 46 vector_3d newAcceleration);
266 Глава 10 47 vector_3d LinearAcceleration(void); 48 49 void Force( 50 force sumExternalForces); 51 force Force(void); 52 53 void CurrentOrientation( 54 angle_set_3d newOrientation); 55 angle_set_3d CurrentOrientation(void); 56 57 void AngularVelocity( 58 vector_3d newAngularVelocity); 59 vector_3d AngularVelocity(void); 60 61 void AngularAcceleration( 62 vector_3d newAngularAcceleration); 63 vector_3d AngularAcceleration(void); 64 65 void RotationalInertia(vector_3d inertiaValue); 66 vector_3d RotationalInertia(void); 67 68 void Torque(vector_3d torqueValue); 69 vector_3d Torque(void); 70 71 void CoefficientOfRestitution( 72 scalar elasticity); 73 scalar CoefficientOfRestitution(void); 74 75 void BoundingSphereRadius(scalar radius); 76 scalar BoundingSphereRadius(void); 77 78 bool Update( 79 scalar changelnTime); 80 bool Render(void); 81 }; В листинге 10.2 приведено определение новой версии класса rigid_ body. Эта версия отличается от версии, использовавшейся в главе 9. Во-первых, объект класса d3d_mesh, объявленный в строке 4, теперь заменен объектом класса mesh. Во-вторых, в строках 22-23 объявлены элементы данных, используемые при моделировании столкновений. Это коэффициент восстановления и радиус ограничивающей сферы. Ранее в этой главе уже отмечалось, что сфера - не лучшая ограничивающая поверхность для тигра. Данная программа позволит вам убедиться в этом. Запустив ее, вы увидите, что тигры отскакивают друг от друга, не соприкасаясь.
Столкновения твердых тел 267 Замечание Основная причина использования в этой программе ограничивающих сфер - стремление упростить код. По этой же причине они используются и в оставшейся части книги. Но я настоятельно рекомендую вам поэкспериментировать с более точными методами обнаружения столкновений. В строках 71-76 приведены прототипы методов чтения и записи значений новых элементов класса. Если мы собираемся использовать платформу физического моделирования для более-менее сложных имитаций, нам понадобятся возможности обнаружения столкновений и просчета их эффектов. Для этого в платформе предназначен класс collision, определение которого приведено в листинге 10.3. Листинг 10.3. Содержимое файла PMCollision.h 1 #ifndef _PMCOLLISION_H 2 #define _PMCOLLISION_H 3 4 namespace pmframework 5 { 6 7 епш collision_status 8 { 9 COLLISION_NONE, 10 COLLISION_TOUCHING, 11 COLLISIONJDVERLAPPING 12 }; 13 14 class collision 15 { 16 private: 17 rigid_body *objectl; 18 rigid_body *object2; 19 20 public: 21 collision (); 22 collision( 23 rigidjbody *firstObject, 24 rigid_body *secondObject); 25 26 void FirstObject( 27 rigid_body *firstObject); 28 rigid_body *FirstObject(void); 29 30 void SecondObject( 31 rigid_body *firstObject); 32 rigid_body *SecondObject(void); 33
268 34 collision_status CollisionOccurred(void); 35 bool CalculateReactions(void); 36 }; 37 38 inline collision::collision() 39 { 40 objectl=object2=NULL; 41 } 42 43 inline collision::collision( 44 rigid_body *firstObject, 45 rigidjbody *secondObject) 46 { 47 assert(firstObject!=NOLL); 48 assert(secondObject!=NOLL); 49 50 objectl=firstObject; 51 objetrt^secondObject; 52 } 53 54 inline void collision::FirstObject( 55 rigid_body *firstObject) 56 { 57 assert(firstObject!=NOLL); 58 59 objectl=firstObject; 60 } 61 62 inline rigidjbody *collision::FirstObject(void) 63 { 64 return (objectl); 65 } 66 67 inline void collision::SecondObject( 68 rigidjbody *secondObject) 69 { 70 assert(secondObject'=NULL); 71 72 object2=secondObject; 73 } 74 75 inline rigidjbody *collision::SecondObject(void) 76 { 77 return (object2J ; 78 } 79 80 } 81 82 #endif
Столкновения твердых тел 269 Первое определение в листинге 10.3 - это перечисление collision status. Этот тип предназначен для выделения возможных вариантов столкновений. Эти варианты показаны на рисунке 10.7. ▼ II т ^ Столкновения нет Соприкосновение Перекрытие Рис. 10.7. Возможные варианты столкновений В любой отдельно взятый момент времени два твердых тела, показанных на рисунке 10.7 как сферы, могут пребывать в одном из трех состояний. Они могут вообще не сталкиваться - и реагировать на столкновение не нужно. Они могут соприкасаться - и нужно просчитывать реакцию на столкновение. Если два тела двигаются быстро, они могут перейти из состояния отсутствия столкновения в состояние перекрытия за один кадр. Столкновения с перекрытием выглядят нереалистично на экране - кажется, что один объект находится внутри другого, хотя так быть не должно. Если происходит столкновение с перекрытием, приходится предпринимать дополнительные меры. В типе collision_status определены константы, позволяющие обозначать три этих типа столкновений.1 Определение класса collision начинается со строки 14 листинга 10.3. Класс содержит private-указатели на два твердых тела. Указатели используются, потому что нужно обновлять данные в объектах, участвующих в столкновении, а не создавать копии этих объектов. В строках 21-32 класса collision содержатся прототипы методов, создающих объект и задающих его элементы. Код этих методов содержится в строках 43-80. Единственные методы, кода которых нет в файле PMCollision.h — это CollisionOccured() и CalculateReactions (). Код этих методов приведен в листинге 10.4. Он содержится в файле PMCollision.срр. Метод CollisionOccuredO начинает работать, предполагая, что столкновения нет. Он вычисляет расстояние между сталкивающимися объектами. Чтобы найти это расстояние, он использует неточный способ, основанный на нахождении расстояния между двумя ограничивающими сферами. Для данного примера этот способ достаточно хорош, и его использование позволяет не усложнять код. 1 Если объекты двигаются очень быстро, и их размеры невелики, столкновения можно вообще не обнаружить. Представьте себе пулю, попадающую в крыло самолета. Скорость пули 700 м/с, а толщина крыла самолета - 0.3 м. Нужно пересчитывать координаты и проверять, есть ли столкновение, порядка 2000 раз в секунду, иначе можем не заметить столкновения (или применять другие методы обнаружения столкновений). - (Прим, перев.).
270 Глава 10 Листинг 10.4. Методы CollisionOccured() и CalculateReactions() 1 collision_status collision::CollisionOccurred(void) 2 { 3 scalar distance; 4 vector_3d distanceVector; 5 collision_status collisionStatus = COLLISIONJJONE; 6 7 // 8 // Находим расстояние между ограничивающими сферами. 9 // 10 distanceVector = objectl->Location() - object2->Location(); 11 distance = AbsValue(distanceVector.Norm()) - 12 objectl->BoundingSphereRadius() - 13 object2->BoundingSphereRadius(); 14 15 // Если расстояние почти нулевое... 16 if (CloseToZero(distance)) 17 { 18 // Ограничивающие сфер» соприкасаются. 19 collisionStatus = COLLISION_TOUCHING; 20 21 } 22 // Иначе, если ограничивавшие сферы перекрываются.... 23 else if (distance < 0.0) 24 { 25 collisionStatus = COLLISION_OVERLAPPING; 26 } 27 28 return (collisionStatus); 29 } 30 31 bool collision::CalculateReactions(void) 32 { 33 /* Находим средний коэффициент восстановления, являющийся 34 мерой эластичности объектов, участвующих в столкновении. */ 35 scalar averageElasticity = 36 (objectl->CoefficientOfRestitution()+ 37 object2->CoefficientOfRestitution())/2; 38 39 // 40 // Теперь вычисляем числитель. 41 // 42 vector_3d relativeVelocity = 43 objectl->AngularVelocity() - object2->AngularVelocity(); 44 vector_3d numerator = 45 -1 * relativeVelocity * (averageElasticity+1);
Столкновения твердых тел 271 46 47 // 48 // Вычисляем знаменатель. Это сложно, поэтому разделим 49 // вычисления на несколько стадий. 50 // 51 /* Сначала находим единичный нормальный вектор. Это 52 нормализованный вектор, направленный иэ центра массы 53 объекта 1 к центру массы объекта 2. */ 54 vector_3d unitNormal = objectl->Location()-object2->Location(); 55 unitNormal = unitNormal.Normalize(SCAIAR_TOLERANCE); 56 57 // Теперь находим точку приложения силы к объекту 2. 58 vector_3d forceLocation2 = 59 unitNormal * object2->BoundingSphereRadius(); 60 61 vector_3d tempVector = forceLocation2.Cross(unitNormal); 62 63 // Делим на инерцию вращения. 64 tempVector.X(tempVector.X()/object2->RotationalInertia().X()); 65 tempVector.Y(tempVector.Y()/object2->RotationalInertia().Y()); 66 tempVector.Z(tempVector.Z()/object2->RotationalInertia().Z()); 67 68 // Вычисляем векторное произведение результата и 69 // вектора г для объекта 2. 70 tempVector = tempVector.Cross(forceLocation2); 71 72 // Вычисляем скалярное произведение вектора на 73 // единичный нормальный вектор. 74 scalar parti = unitNormal.Dot(tempVector); 75 76 // Находим точку приложения силы к объекту 2. 77 unitNormal *= -1; 78 vector_3d forceLocationl = 79 unitNormal * objectl->BoundingSphereRadius(); 80 81 tempVector = forceLocationl.Cross(unitNormal); 82 83 // Делим на инерцию вращения. 84 tempVector.X(tempVector.X()/objectl->RotationalInertia().X()); 85 tempVector.Y(tempVector.Y()/objectl->RotationalInertia().Y()); 86 tempVector.Z(tempVector.Z()/objectl->RotationalInertia().Z()); 87 88 // Вычисляем векторное произведение результата и 89 // вектора г для объекта 1. 90 tempVector = tempVector.Cross(forceLocationl); 91 92 // Вычисляем скалярное произведение вектора на 93 // единичный нормальный вектор. 94 scalar part2 = unitNormal.Dot(tempVector);
272 Глава 10 95 96 scalar denominator = 97 l/objectl->Mass{) + l/object2->Mass() + part2 + parti; 98 99 // 100 // Находим сумму сил для твердого тела 1. 101 // Сначала находим импульсную силу, действующую 102 // при столкновении. 103 force impulseForce; 104 impulseForce.Force(numerator/denominator); 105 impulseForce.ApplicationPoint(forceLocationl); 106 107 // Добавляем постоянные силы (если они есть). 108 vector_3d existingForce = objectl->Force().Force(); 109 // Вычисляем суммарную силу и сохраняем ее в 110 // объекте 1. 111 force totalForce; 112 totalForce.Force(existingForce + impulseForce.Force()); 113 objectl->Force(totalForce); 114 115 // 116 /* Теперь находим сумму сил для твердого тела 2, и 117 сохраняем ее в объекте 2. */ 118 // 119 // Получаем силы, уже действующие на тело 2. 120 existingForce = object2->Force().Force(); 121 122 // Добавляем к ним импульсную силу, 123 // действующую в обратном направлении. 124 totalForce.Force(existingForce - impulseForce.Force()); 125 126 // Сохраняем результат в объекте 2. 127 object2->Force(totalForce); 128 129 return (true); 130 } В строке 16 метод CollisionOccured() проверяет, близко ли полученное расстояние к 0. Используемая для этого функция CloseToZero () содержится в файле PMMathFunctions. h. Из-за погрешностей операций с плавающей запятой лучше не проверять, равен ли результат 0. Если расстояние достаточно близко к 0, чтобы считать его 0, то нужно реагировать на столкновение. Поэтому, если функция CloseToZero () возвращает true, метод CollisionOccured() считает, что сферы соприкасаются.
Столкновения твердых тел 273 Предупреждение О погрешности вычислений с плавающей запятой нужно всегда помнить при написании игр. Один из основных приемов, позволяющих защититься от нее - никогда не проверять, равно ли что-то 0.0. Ответ почти никогда не будет правильным. Проверяйте, близко ли значение к 0.0 настолько, чтобы считаться равным 0.0. Если расстояние не близко к 0, в строке 23 проверяется, не перекрываются ли ограничивающие сферы. При этом расстояние будет меньше 0. Если это так, функция CollisionOccured() присваивает переменной collisionStatus значение, указывающее на столкновение с перекрытием. Если ни одно из условий не выполнилось, то изначально сделанное предположение оказывается верным - столкновения нет. Метод CalculateReactions () начинается со строки 31 листинга 10.4. Он начинается с вычисления коэффициента восстановления столкновения в строках 35-37. Этот коэффициент вычисляется как среднее арифметическое коэффициентов восстановления тел, участвующих в столкновении. Использование такого подхода позволяет учесть эластичность (или отсутствие таковой) всех тел, участвующих в столкновении. Остальная часть функции вычисляет импульсную силу по формуле, учитывающей и линейные, и угловые компоненты. Эта формула еще раз приведена ниже: „ -vr(e+l) rrij m2 Это сложная формула, и вычисление результата по ней в методе CalculateReactions () разбито на несколько этапов. Сначала в строках 42-45 вычисляется числитель. Найти знаменатель сложнее. Его нахождение разделено на несколько шагов и выполняется справа налево. Сначала вычитанием векторов местоположений центров масс и нормализацией результата находится единичный нормальный вектор. В результате получается величина п из формулы. В строках 58-59 метод CalculateReactions () находит вектор, указывающий на точку соприкосновения. Как уже говорилось раньше, мы будем рассматривать точечные соприкосновения между ограничивающими сферами. В результате расчета мы получим величину г2 из выражения в знаменателе формулы. В строке 61 вектор г2 умножается на п. В строках 64-66 результат умножения делится на компоненты вращательной инерции тела 2. Результат деления умножается на г2. На этом заканчиваются расчеты первого справа члена знаменателя. Весь процесс нужно повторить для второго справа члена. Это делается в строках 77-94. Г(г,хп)Л ХГ, п» ^ХПL IV ХГ,
274 Глава 10 В строках 96-97 метод CalculateReactions () завершает вычисление знаменателя. С помощью этого знаменателя он находит вектор импульсной силы, прикладываемый к телу 1 (строка 104). Задав точку, в которой действует импульсная сила, он добавляет импульсную силу ко всем другим силам, действующим на тело. Это могут быть сила тяжести, силы от предыдущего столкновения и другие силы. В любом случае находится общая действующая на тело сила. Импульсную силу нельзя считать единственной действующей на тело, если вы хотите, чтобы игра выглядела реалистичной. Метод CalculateReactions () заканчивается добавлением отрицательной импульсной силы к силам, действующим на второе тело. Поскольку импульсная сила действует на него в противоположном направлении, она вычитается из суммы других сил, а не прибавляется к ней. Разобравшись, как выполняется обнаружение и обработка столкновений, можно посмотреть, как это делает программа. А что? Нужно сделать что-то еще, чтобы моделировать столкновения? Увы, да. Посмотрите на листинг 10.5. Листинг 10.5. Функции обработки столкновений 1 bool UpdateFrame() 2 < 3 static bool forceApplied = false; 4 int i; 5 6 scalar timelncrement = 1; 7 8 DWORD currentTime = ::timeGetTime(); 9 if (!TimeToUpdateFrame(currentTime)) 10 return (true); 11 12 // Для каждого объекта... 13 for (i=0;i<TOTAL_TIGERS-l;i++) 14 { 15 // Ищем столкновения с другими объектами. 16 for (int j=i+l;j<TOTAL_TIGERS;j++) 17 { 18 // Если произошло столкновение... 19 collision theCollision( 20 SallTigers[i], 21 SallTigers[j]); 22 collision_status collisionOccurred = 23 theCollision.CollisionOccurred(); 24 switch (collisionOccurred) 25 { 26 case COLLISIONJTOOCHING: 27 // Просчитываем столкновение. 28 theCollision.CalculateReactions (); 29 // Сила отскока не прикладывалась.
Столкновения твердых тел 275 30 forceApplied=false; 31 break; 32 33 case COLLISIONJDVERLAPPING: 34 // Тигры перекрываются. Выполняем откат. 35 HandleOverlapping( 36 timelncrement,i,j,theCollision); 37 forceApplied=false; 38 break; 39 40 case COLLISION_NONE: 41 // Здесь ничего не нужно делать. 42 // Добавлено только для завершенности. 43 break; 44 } 45 ) 46 } 47 48 // Если силы уже были приложены... 49 if (forceApplied) 50 { 51 // Уменьшаем силы до 0. 52 for (i=0;i<TOTAL_TIGERS;i++) 53 { 54 force theForce; 55 theForce. Force (vector_3d @.0,0.0,0. Of") ) ; 56 theForce.ApplicationPoint(vector_3d@.0,0.0,0.0)) 57 allTigers[i].Force(theForce); 58 } 59 } 60 // Иначе нужно приложить силы. 61 else 62 { 63 forceApplied=true; 64 } 65 66 // Обновляем данные о каждом тигре. 67 for (i=0;i<TOTAL_TIGERS;i++) 68 ( 69 allTigers[i].Update(timelncrement); 70 > 71 72 return (true); 73 > 74 75 bool TimeToUpdateFrame( 76 DWORD currentTime) 77 { 78 // Эта инициализация выполняется только однажды.
276 Глава 10 79 static DWORD lastTime=0; 80 81 // Эта инициализация выполняется при каждом вызове функции. 82 bool updateFrame=false; 83 84 // Если прошло достаточно миллисекунд... 85 if (currentTime-lastTime >= MILLISECONDS_PER_FRAME) 86 { 87 // Пора просчитывать новый кадр. 88 updateFrame=true; 89 90 // Сохраняем время последнего обновления кадра. 91 lastTimeecurrentTime; 92 } 93 return (updateFrame); 94 > 95 96 void HandleOverlapping{ 97 scalar timelncrement, 98 int tigerl, 99 int tiger2, 100 collision fitheCollision) 101 { 102 scalar changelnTime = timelncrement; 103 104 // Произошло столкновение с перекрытием. 105 collision_status collisionOccured = 106 COLLISION_OVERLAPPING; 107 108 // Пока не просчитали и инкремент времени не нулевой... 109 for (bool done=false; 110 (!done) && ('CloseToZero(changelnTime)); 111 /* Нет ни инкремента, ни декремента */) 112 { 113 // Проверим тип столкновения. 114 switch (collisionOccured) 115 { 116 // Если ограничивающие сферы все еще перекрываются... 117 case COLLISION_OVERLAPPING: 118 { 119 rigid_body objectl = allTigers[tigerl]; 120 rigid_body object2 = allTigers[tiger2]; 121 122 // Обращаем направления скоростей и сил. 123 vector_3d tempVector = 124 objectl.AngularVelocity(); 125 tempVector *= -1; 126 objectl.AngularVelocity(tempVector); 127 tempVector = objectl.LinearVelocity{);
Столкновения твердых тел 277 128 tempVector *= -1; 129 objectl.LinearVelocity(tempVector); 130 objectl.Force().Force( 131 objectl.Force().Force() * -1); 132 133 // Обращаем направления скоростей и сил. 134 tempVector = 135 object2.AngularVelocity(); 136 tempVector *= -1; 137 object2.AngularVelocity(tempVector) ; 138 tempVector = object2.LinearVelocity(); 139 tempVector *= -1; 140 object2.LinearVelocity(tempVector); 141 object2.Force().Force( 142 object2.Force().Force() * -1); 143 144 // Выполняем откат по времени. 145 objectl.Update(changelnTime) ; 146 object2.Update(changelnTime); 147 148 // Делаем меньший шаг по времени. 149 changeInTime/=2; 150 151 // Готовимся опять двигаться вперед. 152 153 /* Задаем скорости и силы для движения вперед. */ 154 tempVector = 155 objectl.AngularVelocity(); 156 tempVector *= -1; 157 objectl.AngularVelocity(tempVector); 158 tempVector = objectl.LinearVelocity(); 159 tempVector *= -1; 160 objectl.LinearVelocity(tempVector); 161 objectl.Force().Force( 162 objectl.Force().Force() * -1) ; 163 164 /* Задаем скорости и силы для движения вперед. */ 165 tempVector = 166 object2.AngularVelocity(); 167 tempVector *= -1; 168 object2.AngularVelocity(tempVector); 169 tempVector = object2.LinearVelocity(); 170 tempVector *= -1; 171 object2.LinearVelocity(tempVector); 172 object2.Force().Force( 173 object2.Force().Force() * -1); 174 175 // Двигаемся вперед на меньшую величину. 176 objectl.Update(changelnTime);
278 Глава 10 177 ob j ect2.Update(changeInT ime) ; 178 179 allTigers[tigerl] = objectl; 180 allTigers[tiger2] = object2; 181 182 // Опять проверяем вид столкновения. 183 colllsionOccured = 184 theCollision.CollisionOccurred(); 185 } 186 break; 187 188 // Если ограничивающие сферы теперь соприкасаются... 189 case COLLISION_TOOCHING: 190 // Просчитываем столкновение. 191 theCollision.CalculateReactions(); 192 done=true; 193 break; 194 195 // Если столкновения теперь нет... 196 case COLLISION_NONE: 197 // Отступили слишком далеко. Двигаемся вперед. 198 allTigersttiger1].Update(changelnTime); 199 allTigers[tiger2].Update(changelnTime); 200 201 // Опять проверяем вид столкновения. 202 collisionOccured = 203 theCollision.CollisionOccurred(); 204 break; 205 } 206 } 207 /* Если цикл завершился, поскольку временной шаг 208 стал почти нулевым... */ 209 if (CloseToZero(changelnTime)) 210 { 211 // Просчитываем столкновение. 212 theCollision.СаleulateReactions(); 213 allTigers[tiger1].Update(changelnTime); 214 allTigers[tiger2].Update(changelnTime); 215 } 216 } Основную часть работы выполняет функция UpdateFrame (). Платформа физического моделирования вызывает ее в каждой итерации основного цикла игры. Функция должна обновлять информацию обо всех объектах в сцене - в данном случае это три тигра. Она должна использовать класс collision для обнаружения столкновений и вычисления появляющихся в результате сил. Применив эти силы в одном кадре, функция UpdateFrame () должна отменить их в следующем. Если помните, это импульсные силы, и они действуют в течение коротких промежутков времени.
Столкновения твердых тел 279 Сначала UpdateFrame () устанавливает переменную состояния f ог- ceApplied в значение false. Это значит, что силы, действующие на твердые тела в сцене, еще не прикладывались в текущем кадре. Твердые тела еще не анимировались с учетом этих сил. В строке 6 задается временной шаг, равный 1. Вспомните, в функции Gamelnitialization() масса тигров задана равной 100 кг. Единицы измерения силы - кг • м/с2. Поэтому временной шаг равен 1 секунде. Замечание Временной шаг выбран таким, чтобы программа нормально выполнялась на машинах, видеокарты которых не поддерживают в полном объеме возможности DirectX 9. Этот пример использует аппаратные вертексные процессоры, если они есть. Если нет, используется программная обработка вертексов. Если ваша видеокарта не высшего класса и старше 2-3 лет, она, возможно, не содержит аппаратных вертексных процессоров. Для таких машин нужно задавать временной шаг порядка секунды, чтобы движение на экране было достаточно плавным. Если вы обнаружите, что программа работает слишком быстро и дает дерганую, неравномерную анимацию, попробуйте ИЗМеНИТЬ временной шаг HaMILLISECONDS_PER_FRAME/1000. ЭТО сделает временной шаг зависящим от частоты смены кадров - 30 кадров в секунду. Анимация будет плавной и качественной для тех, у кого видеокарты поддерживают аппаратную обработку вертексов. Затем UpdateFrame () получает значение текущего времени и вызывает функцию TimeToUpdateFrame (). Код функции TimeToUpdateFrame () приведен в строках 75-94 листинга 10.5. Эта функция проверяет, прошло ли 33 миллисекунды после предьщущего обновления кадра. Если да, функция возвращает true, в противном случае она возвращает false. Эта функция ограничивает частоту смены кадров величиной 30 кадров в секунду. Если со времени последнего обновления кадра прошло меньше 33 миллисекунд, то функция UpdateFrame () просто возвращает управление (строки 9-10). Если пора обновлять кадр, функция UpdateFrame () перебирает список тигров с помощью пары вложенных циклов. Каждый тигр проверяется на предмет наличия столкновений с остальными тиграми из списка. В строках 19-23 создается переменная типа collision и выполняется проверка наличия столкновения между тиграми. Результаты проверки обрабатываются в операторе switch, начинающемся со строки 24. Если ограничивающие сферы соприкасаются, функция UpdateFrame () вызывает метод collision : : CalculateReactions (), прикладывающий к столкнувшимся тиграм силы, возникающие при столкновении. Если столкновения нет, функция UpdateFrame () не делает ничего. Замечание Думаю, понятно, что ветвь оператора switch, соответствующая значению COLLISION_none, не обязательна. Она присутствует в программе только в иллюстративных целях.
280 Глава 10 Наиболее сложен случай с перекрытием ограничивающих сфер. В этом случае функция UpdateFrame () вызывает функцию HandleOverlap- ping(). Код функции HandleOverlappingO приведен в строках 96-216 листинга 10.5. Функция HandleOverlappingO исправляет столкновения с перекрытием, выполняя шаги обратно во времени игры. В строках 122-142 листинга 10.5 она меняет на обратные направления векторов сил, линейных и угловых скоростей, действующих на оба перекрывающихся объекта. Затем выполняется обновление данных в обоих объектах. Эффект от этих действий эквивалентен возврату во времени в момент до столкновения. В строке 149 функция HandleOverlapping() делит пополам временной шаг. Строки 154-173 восстанавливают исходные направления сил, линейных и угловых скоростей обоих тел. Затем выполняется обновление, и функция HandleOverlappingO проверяет, произошло ли столкновение (строки 183-184). Затем исполнение продолжается со строки 109, с которой начинается цикл for. Оператор switch, начинающийся в строке 114, опять определяет реакцию на стодкноъенже. Если возврат назад во времени приводит к тому, что объекты соприкасаются, функция HandleOverlappingO вычисляет силы, возникающие при столкновении, и завершает цикл. Если тела все еще перекрываются, HandleOverlapping () еще раз выполняет возврат во времени, уменьшает вдвое временной шаг и повторяет все снова. Это повторяется до тех пор, пока тела будут только соприкасаться, но не перекрываться. Возможно, в результате возврата тела переместятся назад слишком далеко. В этом случае столкновения не будет, и нужно будет двигаться вперед во времени до тех пор, пока тела не соприкоснуться. Это делает код в строках 196-204. После того, как функция HandleOverlappingO добьется того, что тела будут соприкасаться, но не перекрываться, и приложит к ним импульсную силу, она вернет управление функции UpdateFrame (), а точнее, в строку 37 листинга 10.5. Поскольку к телам приложены новые силы, функция UpdateFrame () сбрасывает в false значение переменной forceApplied. Когда выполнение доходит до строки 49, проверяется значение этой переменной. Если оно равно true (то есть силы уже прикладывались к телам, и вновь их прикладывать не нужно), функция UpdateFrame () уменьшает до 0 силы, действующие на тигров. Если силы еще не прикладывались, функция устанавливает переменную forceApplied в значение true в строке 59. При этом силы будут уменьшены до 0 при следующем вызове функции UpdateFrame (). Завершая работу, функция UpdateFrame О еще раз перебирает всех тигров, обновляя данные о них. Теперь они будут занимать правильные положения, и их можно отображать на экране функцией RenderFrame (). Код функции RenderFrameO здесь не приведен - он вполне тривиален. Если хотите, можете просмотреть его в файле TigerToss. cpp на компакт-диске.
Столкновения твердых тел 281 Итоги Вы увидели, как моделируются линейные и вращательные движения твердых тел, и узнали, как обнаруживать столкновения между ними и реагировать на них. Когда вы запускаете программу из этой главы, она выполняет множество операций. Она действительно моделирует поведение твердых тел из реального мира. Это весьма впечатляюще. Но это отнюдь не все. В следующей главе мы разберемся, как моделировать силу тяжести.
Глава 11 Сила тяжести и метательные снаряды В главе 10 «Столкновения твердых тел» мы разобрались, как моделировать столкновения твердых тел в средах без силы тяжести и трения. Однако большинство из нас не живут в таких средах. Чтобы сделать игры более реалистичными, нужно хотя бы учесть в них силу тяжести. Добавить ее в игры несложно. Она легко реализуется, если следовать идеям, изложенным в предыдущих главах. Закон всемирного тяготения Ньютона Сила тяжести или гравитации всегда присутствует вокруг вас. От нее невозможно избавиться - даже в космосе. Все, находящееся на Земле, остается поблизости от Земли из-за действия этой силы. Земля удерживается на орбите вокруг Солнца тоже благодаря действию силы тяжести. Куда бы вы ни отправились, уйти от силы тяжести вам не удастся. Сэр Исаак Ньютон сформулировал свой закон всемирного тяготения, когда ему было всего лишь 23 года. В 1665 году он уехал из Кембриджа, в котором преподавал в колледже, в Линкольншир - сельскую область Англии. В том году колледж был закрыт из-за эпидемии чумы. Все, кто мог, покидали города, поскольку именно в них чума свирепствовала больше всего. Замечание Иногда можно услышать, что Ньютон «открыл» силу тяжести. Это неточное выражение. Чтобы открыть что-то, нужно быть первым, это что-то заметившим. Ньютон наверняка был не первым человеком, заметившим, что лишившиеся опоры предметы падают. Суть открытия Ньютона, сделанного им во время отдыха в Линкольншире, была в следующем. Он понял, что сила, заставляющая объекты падать на землю, это та же сила, что удерживает Луну на орбите вокруг Земли. Такое обобщение позволило ему понять, что сила гравитации
Сила тяжести и метательные снаряды 283 слабеет с увеличением расстояния между объектами. Собственно говоря, он смог вычислить, что сила гравитации убывает пропорционально квадрату расстояния между телами и растет пропорционально их массе. Это значит, что очень тяжелые объекты обладают сильным гравитационным полем. Легкие объекты обладают настолько слабым полем, что заметить его в повседневной жизни невозможно. Кроме того, Ньютон понял, что по мере удаления объектов друг от друга сила притяжения между ними быстро уменьшается. Яблоко НЕ падало Ньютону на голову! Существует популярный исторический анекдот, гласящий, будто бы Ньютон «открыл» силу тяжести, после того, как упавшее с дерева яблоко ударило его по голове. Эта история - всего лишь миф, появившийся благодаря разговору Ньютона с другом почти через 50 лет после его пребывания в Линкольншире. Во время чаепития с Уильямом Стакели в яблоневом саду Ньютон сказал, что обстановка здесь почти такая же, как та, в которой у него зародилась идея о силе тяжести. Ньютон рассказал, что в 1665 году он сидел под яблоней, погрузившись в размышления, когда заметил, как упало яблоко. Оно не упало ему на голову. Этот случай положил начало цепочке идей, которые, в конце концов, оформились в закон всемирного тяготения. Силу притяжения между двумя телами можно вычислить, начиная с той же формулы, которую мы использовали в качестве отправной точки почти во всех рассуждениях в этой книге: F = та. Чтобы вычислить силу притяжения между двумя объектами, нужно учесть массу каждого из них. Как обнаружил Ньютон, ускорение, испытываемое объектами, обратно пропорционально квадрату расстояния между ними. Однако расстояние - не единственный множитель. Есть еще и универсальная константа, одинаковая для любых частиц, размер которых больше, чем размер атомов. Эта константа обозначается G (универсальная гравитационная постоянная). Вот ее значение: G = 6.673 х КГПм3/(кг-с2) Учтя G и массу обоих объектов, формулу F = та можно записать в виде ГПтГП-, F = G У г1 Обратите внимание, что на оба объекта действуют одинаковые по величине, но противоположные по направлению силы - объекты движутся навстречу друг другу. Если бросить мяч с крыши дома (не стоит этого делать), мяч начнет падать. Сила тяжести, действующая на мяч, равна по величине силе тяжести, действующей на Землю. Однако вы не заметите
284 Глава 11 перемещения Земли. Это потому, что, хотя силы, действующие на мяч и на Землю, равны по величине, масса Земли колоссальна по сравнению с массой мяча. Поэтому ускорение Земли неуловимо мало по сравнению с ускорением мяча, и мяч движется к Земле, а Земля остается практически неподвижной. Ускорение, с которым движутся объекты в гравитационном поле Земли, практически постоянно вблизи Земли. Говоря «практически постоянно», я подразумеваю, что оно не изменяется сколько-нибудь заметно. Да, оно немножко меньше, когда вы высоко в горах, и немножко больше, когда вы на берегу моря. Но изменение настолько незначительно, что им можно пренебречь. Поэтому будем считать ускорение силы тяжести на Земле (g) постоянной величиной, равной -9.8 м/с2. В физике g часто считается вектором, направленным к центру Земли. Обычно это отрицательное направление вдоль вертикальной оси координат, но это не обязательно. Однако в декартовых системах координат, используемых в физике и компьютерной графике, чаще всего это именно так, и мы будем придерживаться этого стандарта. Будем считать величину вектора g равной -9.8 м/с2, и минус будет напоминать нам, что вектор g указывает вниз. Не забывайте, что G - не то же самое, что g. G - это универсальная гравитационная постоянная, ag- ускорение силы тяжести вблизи Земли. Траектории метательных снарядов Поскольку ускорение силы тяжести вблизи Земли постоянно, все метательные снаряды двигаются по предсказуемым траекториям. Обычно под словом «метательный снаряд» мы подразумеваем артиллерийский снаряд или ракету. Но в данном обсуждении мы будем считать метательным снарядом любой объект, который можно бросить или уронить. Посмотрим на силы, действующие на брошенный предмет, например, мяч. Пока проигнорируем силы, обусловленные действием ветра или сопротивлением воздуха. Если просто выпустить мяч из рук, мы не приложим к нему никаких сил, но Земля приложит к нему силу. Поэтому сила тяжести - это единственная сила, которая определяет движение мяча в данном случае. На рисунке 11.1 показана сила тяжести, действующая на мяч. Как видно из рисунка 11.1, мяч будет двигаться прямо вниз. Из повседневного опыта мы знаем, что под действием силы тяжести тела двигаются по направлению к центру Земли. В играх, как и в жизни, это значит, что гравитация прикладывает ко всему вертикально направленную силу. А что, если толкнуть мяч в горизонтальном направлении, выпуская его? Например, предположим, что мы сбросили мяч с летящего самолета, как на рисунке 11.2.
Сила тяжести и метательные снаряды 285 тяжести Рис. 11.1. Сила тяжести, действующая на падающий мяч горизонтальная ' вертикальная Рис. 11.2. Метательный снаряд, движущийся и по горизонтали, и по вертикали Горизонтальное движение самолета приводит к действию на мяч силы, направленной по горизонтали. Конечно, на самолет и мяч действует и сила тяжести. Когда мяч сбрасывается с самолета, горизонтальная
286 Глава 11 сила перестает действовать. Однако если не учитывать сопротивление воздуха, мяч продолжит двигаться по горизонтали с той же скоростью, что и самолет. Кроме того, мяч начнет набирать вертикальную скорость, поскольку на него будет действовать сила тяжести, которой больше не будет противодействовать подъемная сила самолета. Чтобы найти общую скорость мяча, нужно сложить скорости по вертикали и горизонтали. На рисунке 11.3 показана траектория движения мяча, сброшенного с самолета. Параболическая траектория движения выпущенного мяча "горизонтальная 'вертикальная Рис. 11.3. Траектория полета метательного снаряда, движущегося по горизонтали с определенной скоростью Траектория сброшенного метательного снаряда, обладающего начальной горизонтальной скоростью - это часть параболы. Вообще, любое тело, обладающее скоростью, перпендикулярной направлению гравитационного поля, опишет в этом поле параболу или ее часть. Мяч на рисунке 11.3 описывает половину параболы. Пули ведут себя точно так же. При выстреле они приобретают большую скорость под воздействием силы, возникающей при сгорании пороха. Если пуля ни во что не попадает, летя горизонтально, она может улететь далеко. Но постепенно она снижается под воздействием силы тяжести. Траектория пули, выстрелянная горизонтально, тоже будет половиной параболы, только растянутой по горизонтали. Именно поэтому снайперы метят чуть выше цели, стреляя на большое расстояние - они учитывают воздействие силы тяжести на пули. Все брошенные предметы ведут себя одинаково. В играх брошенные предметы могут быть разными - от мячей до гранат. Но все они двигаются по параболам, как показано на рисунке 11.4.
Сила тяжести и метательные снаряды 287 / / / \ / \ \ \ Рис. 11.4. Брошенные предметы движутся по параболам Снаряды, которыми, например, выстрелили из пушки, тоже движутся по параболам. Если точка падения снаряда расположена на той же высоте, что и точка выстрела, то снаряд опишет симметричную кривую - фрагмент параболы. Если точка падения расположена ниже точки выстрела, то снаряд будет продолжать двигаться по параболе. Если точка падения выше точки выстрела, то снаряд опишет более короткий фрагмент параболы. Все эти рассуждения подразумевают, что на снаряд в полете не действуют никакие силы, кроме силы тяжести. Если снаряд врежется во что-то, на него подействует импульсная сила, которая изменит траекторию его движения. Если снаряд является ракетой, на него будет действовать сила, возникающая при сгорании топлива. Под воздействием этой силы ракета может двигаться по непараболической траектории. Пока мы не будем рассматривать движение ракет. Все бросаемые предметы в играх должны перемещаться по параболическим траекториям. Это относится и к снарядам, и к пулям. К счастью, этого несложно добиться. Вам вообще не нужно ничего знать об уравнениях параболических кривых. Моделирование движения метательных снарядов Подумайте о движении предметов в реальном мире. Никто не просчитывает траекторий их движения. Снаряды движутся по параболам под воздействием действующих на них сил. Моделируя движение этих снарядов, достаточно моделировать действие этих сил. Если модель их действия соответствует их действию в реальном мире, метательные снаряды будут вести себя так же, как и в реальном мире.
288 Глава 11 Замечание Код версии симулятора твердых тел, поддерживающего силу тяжести, можно найти в папке Source\Chapterll\Launcher на компакт-диске, поставляющемся с книгой. Как и в реальном мире, при моделировании не нужно просчитывать траектории движения снарядов. Все, что нужно, - это моделировать действующие на них силы. Результатом правильного моделирования будут правильные параболические траектории. Разделение импульсных и постоянно действующих сил Вспомните, в главе 10 мы рассматривали класс rigid__body, моделирующий поведение реальных объектов под воздействием приложенных к ним сил. Чтобы учесть в нашей модели силу тяжести, нужно отделить друг от друга импульсные и постоянно действующие силы. Как показано в главе 10, импульсные силы действуют на объекты при столкновениях. Они также могут действовать на снаряды в момент их запуска (броска или выстрела). Эти силы действуют в течение коротких промежутков времени. В играх это обычно значит, что они действуют в течение одной итерации моделирования (или одного кадра анимации). После этого действие импульсных сил должно прекращаться. Но постоянно действующие силы действуют во всех итерациях моделирования. Чтобы добавить силу тяжести в модель из главы 10, программа должна отдельно обрабатывать постоянно действующие и импульсные силы. Для этого нужно внести некоторые изменения в класс rigid_body (см. листинг 11.1). Листинг 11.1. Обновленный класс rigidbody 1 class rigid_body 2 { 3 private: 4 mesh objectMesh; 5 6 // Физические свойства и характеристики поступательного 7 // движения. 8 scalar mass; 9 vector_3d centerOfMassLocation; 10 vector_3d linearVelocity; 11 vector_3d linearAcceleration; 12 force constantForce; 13 force impulseForce; 14
Сила тяжести и метательные снаряды 289 15 // Характеристики вращательнох'о движения. 16 angle_set_3d CurrentOrientation; 17 vector_3d angularVelocity; 18 vector_3d angularAcceleration; 19 vector_3d rotationallnertia; 20 vector_3d torque; 21 22 // Характеристики столкновений. 23 scalar coefficientOfRestitution; 24 scalar boundingSphereRadius; 25 26 D3DXMATRIX worldMatrix; 27 28 public: 29 rigid_body(void); 30 31 bool LoadMesh( 32 std::string meshFileName); 33 34 void Mass( 35 scalar massValue); 36 scalar Mass(void); 37 38 void Location( 39 vector_3d locationCenterOfMass); 40 vector_3d Location(void); 41 42 void LinearVelocity( 43 vector_3d newVelocity); 44 vector_3d LinearVelocity(void); 45 46 void LinearAcceleration( 47 vector_3d newAcceleration); 48 vector_3d LinearAcceleration(void); 49 50 void ConstantForce( 51 force sumConstantForces); 52 force ConstantForce(void); 53 54 void ImpulseForce( 55 force sumlmpulseForces); 56 force ImpulseForce(void); 57 58 void CurrentOrientation( 59 angle_set_3d newOrientation); 60 angle_set_3d CurrentOrientation(void); 61
290 Глава 11 62 void AngularVelocity( 63 vector_3d newAngularVelocity); 64 vector_3d AngularVelocity(void); 65 66 void AngularAcceleration( 67 vector_3d newAngularAcceleration); 68 vector_3d AngularAcceleration(void); 69 70 void Rotationallnertia(vector_3d inertiaValue); 71 vector_3d Rotationallnertia(void); 72 73 void Torque(vector_3d torqueValue); 74 vector_3d Torque(void); 75 76 void CoefficientOfRestitution( 77 scalar elasticity); 78 scalar CoefficientOfRestitution(void); 79 80 void BoundingSphereRadius(scalar radius); 81 scalar BoundingSphereRadius(void); 82 83 bool Update( 84 scalar changelnTime); 85 bool Render(void); 86 }; Чтобы обеспечить моделирование силы тяжести и других сил, постоянно действующих, в класс rigid_body нужно внести лишь незначительные изменения. Теперь в классе два элемента для хранения сил, а не один. В строках 12-13 листинга 11.1 приведены определения двух элементов данных типа force. В первом хранится сумма всех постоянно действующих сил, приложенных к объекту, а во втором - сумма всех приложенных к объекту импульсных сил. Постоянно действующие силы в этой версии класса rigid_body прикладываются к центру массы твердого тела. Сила тяжести действует именно таким образом. Если вы моделируете запуск ракеты, то тяга двигателей ракеты действует вдоль продольной оси ракеты. Эта ось проходит через центр массы, поэтому класс rigid_body можно использовать для моделирования простых ракет и метательных снарядов. Существуют и силы, которые могут действовать на твердое тело в направлении, не проходящем через центр его массы. Это так называемые «внеосевые» силы. Пример таких сил - силы, возникающие при работе двигателей системы ориентации космического корабля.' Эти двигатели должны разворачивать корабль, поэтому их векторы тяги не должны указывать на центр массы корабля. Пока они работают, двигатели системы ориентации будут источниками сил, действующих на корабль. Данная версия класса rigid_body не позволяет моделировать такие силы.
Сила тяжести и метательные снаряды 291 В строках 50-52 листинга 11.1 содержатся прототипы двух методов. Это методы чтения и записи величин постоянно действующих сил для объектов класса rigid_body. В строках 54-56 содержатся прототипы аналогичных методов для работы с импульсными силами. Поскольку класс rigid_body теперь может работать отдельно с постоянно действующими и отдельно с импульсными силами, нужно внести некоторые изменения в метод Update (). Код новой версии этого метода приведен в листинге 11.2. Листинг 11.2. Версия метода rigid_body::Update(), работающая с постоянно действующими и импульсными силами 1 bool rigid_body::Update( 2 scalar changelnTime) 3 { 4 // 5 /* Начинаем с расчета линейной динамики. Ее определяют 6 силы, действующие на центр массы. */ 7 // 8 9 // Суммируем силы, действующие на твердое тело. 10 force sumForces; 11 sumForces.Force( 12 constantForce.Force() + impulseForce.Force()); 13 14 // Находим линейное ускорение. 15 // a = F/m 16 assert(mass!=0); 17 linearAcceleration = sumForces.Force()/mass; 18 19 // Находим линейную скорость. 20 linearVelocity += linearAcceleration * changelnTime; 21 22 // Находим новое положение центра массы. 23 centerOfMassLocation += linearVelocity * changelnTime; 24 25 // 26 // Линейная динамика просчитана. 27 // 28 29 // Создаем матрицу перемещения. 30 D3DXMATRIX totalTranslation; 31 D3DXMatrixTranslation( 32 StotalTranslation, 33 centerOfMassLocation.X(), 34 centerOfMassLocation.У(), 35 centerOfMassLocation.Z()); 36
292 Глава 11 37 // 38 // Начинаем расчет вращательной динамики. 39 // 40 41 //По известной импульсной силе находим вращающий момент. 42 torque = 43 impulseForce.ApplicationPoint().Cross(impulseForce.Force()); 44 45 /* По вращающему моменту и инерции вычисляем 46 угловое ускорение.*/ 47 angularAcceleration.X( 48 torque.X()/rotationalInertia.X()); 49 angularAcceleration.Y( 50 torque.Y()/rotationallnertia.Y()); 51 angularAcceleration.Z( 52 torque.Z()/rotationallnertia.Z()); 53 54 /* Изменяем угловую скорость согласно угловому ускорению. */ 55 angularVelocity += angularAcceleration * changelnTime; 56 57 // 58 // Используем угловое ускорение, чтобы найти углы вращения. 59 // 60 currentOrientation.XAngle( 61 currentOrientation.XAngle() + 62 angularVelocity.X() * changelnTime); 63 currentOrientation.YAngle( 64 currentOrientation.YAngle() + 65 angularVelocity.Y() * changelnTime); 66 currentOrientation.ZAngle( 67 currentOrientation.ZAngle() + 68 angularVelocity.Z() * changelnTime); 69 70 // 71 // Завершили расчет вращательной динамики. 72 // 73 74 // Создаем матрицы вращения для каждой оси. 75 D3DXMATRIX rotationX, rotationY, rotationZ; 76 D3DXMatrixRotationX(&rotationX,currentOrientation.XAngle()); 77 D3DXMatrixRotationY(SrotationY,currentOrientation.YAngle()); 78 D3DXMatrixRotationZ(SrotationZ,currentOrientation.ZAngle()); 79 80 D3DXMATRIX totalRotations; 81 82 // Перемножаем их, чтобы получить глобальную матрицу. 83 D3DXMatrixMultiply( 84 StotalRotations, 85 SrotationX,
Сила тяжести и метательные снаряды 293 86 SrotationY); 87 D3DXMatrixMultiply( 88 StotalRotations, 89 StotalRotations, 90 SrotationZ); 91 92 /* Объединяем матрицы вращения и перемещения 93 в глобальную матрицу. */ 94 D3DXMatrixMultiply( 95 SworldMatrix, 96 StotalRotations, 97 StotalTranslation); 98 99 // 100 // Импульсные силы приложены. Обнуляем их. 101 // 102 vector_3d tempVector@.0,0.0,0.0); 103 impulseForce.Force(tempVector); 104 impulseForce.ApplicationPoint(tempVector); 105 106 return(true); 107 } Метод rigid_body: : Update () в листинге 11.2 начинается с объявления переменной sumForces. Поскольку класс rigid_body теперь отдельно обрабатывает импульсные силы и постоянно действующие, метод Update () должен их суммировать, чтобы найти общую силу, действующую на твердое тело. Это делается, когда моделируются линейные силы, действующие на твердое тело. Когда метод Update () просчитывает вращательную динамику, он не учитывает постоянно действующие силы. Как я уже говорил, это потому, что он не учитывает возможности существования внеосевых постоянно действующих сил. Пока класс подразумевает, что все постоянно действующие силы приложены к центру массы. В строке 42 листинга 11.2 начинается вычисление вращающего момента тела по импульсным силам. До того, как выполнение метода закончится, эти силы будут уменьшены до 0 в строках 102-104. Раньше это делала функция UpdateFrame (), не являющаяся методом класса ri- gid_body. Но работать с силами в методе Update () проще и этот подход выглядит аккуратнее с точки зрения структуры программы. Еще одна функция, в которую нужно внести изменения, - это метод collision: :CalculateReactions (). Вспомните, в главе 10 этот метод просчитывал силы, возникающие в результате столкновения. Безусловно, эти силы являются импульсными. Теперь, поскольку класс rigid_body обрабатывает импульсные силы отдельно от постоянно действующих, нужно, чтобы метод CalculateReactions () учитывал только импульсные силы. В листинге 11.3 приведен код новой версии метода CalculateReactions ().
294 Глава 11 Листинг 11.3. Версия метода CalculateReactionsO, учитывающая только импульсные силы 1 bool collision::CalculateReactions(void) 2 { 3 /* Вычисляем средний коэффициент восстановления, который 4 определяет эластичность сталкивающихся объектов. */ 5 scalar averageElasticity = 6 (objectl-XToefficientOfRestitution()+ 7 object2->CoefficientOfRestitution())/2; 8 9 // 10 // Вычисляем числитель. 11 // 12 vector_3d relativeVelocity = 13 objectl->AngularVelocity()-object2->AngularVelocity(); 14 vector_3d numerator = 15 -1 * relativeVelocity * (averageElasticity+1); 16 17 // 18 // Находим знаменатель. Это сложно, поэтому делается 19 //в несколько шаров. 20 // 21 /* Сначала находим единичный нормальный вектор, направленный 22 из центра массы объекта 1 к центру массы объекта 2. */ 23 vector_3d unitNormal=objectl->Location()-object2->Location(); 24 unitNormal = unitNormal.Normalize(SCALAR_TOLERANCE); 25 26 // Теперь находим точку приложения сил к объекту 2. 27 vector_3d forceLocation2 = 28 unitNormal * object2->BoundingSphereRadius(); 29 30 vector_3d tempVector = forceLocation2.Cross(unitNormal); 31 32 // Делим на инерцию вращения. 33 tempVector.X(tempVector.X() / 34 object2->RotationalInertia().X()); 35 tempVector.Y(tempVector.Y() / 36 object2->RotationalInertia().Y()); 37 tempVector.Z(tempVector.Z() / 38 object2->RotationalInertia().Z()) ; 39 40 // Перемножаем ответ с вектором г для объекта 2. 41 tempVector = tempVector.Cross(forceLocation2); 42 43 // Перемножаем с единичным нормальным вектором. 44 scalar parti = unitNormal.Dot(tempVector); 45
Сила тяжести и метательные снаряды 295 46 // Теперь находим точку приложения сил к объекту 2. 47 unitNormal *= -1; 48 vector_3d forceLocationl = 49 unitNormal * objectl->BoundingSphereRadius(); 50 51 tempVector = forceLocationl.Cross(unitNormal); 52 53 // Делим на инерцию вращения. 54 tempVector.X(tempVector.X() / 55 objectl->RotationalInertia().X()); 5 6 tempVector.Y(tempVector.Y() / 57 objectl->RotationalInertia().Y()) ; 58 tempVector.Z(tempVector.Z() / 59 objectl->RotationalInertia() . Z()) ; 60 61 // Перемножаем ответ с вектором г для объекта 2. 62 tempVector = tempVector.Cross(forceLocationl); 63 64 // Перемножаем с единичным нормальным вектором. 65 scalar part2 = unitNormal.Dot(tempVector); 66 67 scalar denominator = 68 l/objectl->Mass() + l/object2->Mass() + part2 + parti; 69 70 // 71 // Прикладываем импульсную силу к объекту 1. 72 // 73 force impulseForce; 74 impulseForce.Force(numerator/denominator); 75 impulseForce.ApplicationPoint(forceLocationl); 76 objectl->ImpulseForce(impulseForce); 77 78 // 79 // Прикладываем импульсную силу в обратном направлении к 80 // объекту 2. 81 // 82 impulseForce.Force( 83 -l*impulseForce.Force()); 84 object2->ImpulseForce(impulseForce); 85 86 return (true); 87 } В версии метода collision: :CalculateReactions (), приведенной в листинге 11.3, для вычисления реакций твердых тел на столкновение используются только импульсные силы. Импульсные силы прикладываются к обоим телам (строки 73-84).
296 Глава 11 Теперь платформа готова поддерживать силу тяжести. Однако прежде чем программа примера сможет отобразить снаряды, врезающиеся в поверхность земли, в программе должна присутствовать эта поверхность, в которую можно врезаться. Поэтому нужно создать объект типа ground. Затем этот объект и силу тяжести нужно добавить в программу примера. Код класса ground приведен в листинге 11.4. Листинг 11.4. Содержимое файла ground.h I #include "PMFramework.h" 2 3 using namespace pmframework; 4 5 class ground 6 { 7 private: 8 vector_3d location; 9 mesh groundMesh; 10 II public: 12 ground(); 13 14 void Location(vector_3d newLocation); 15 vector_3d Location(); 16 17 bool LoadMesh(std::string meshFileName); 18 19 bool Render(void); 20 }; 21 22 inline ground::ground() 23 { 24 } 25 26 27 inline void ground::Location(vector_3d newLocation) 28 { 29 location = newLocation; 30 } 31 32 inline vector_3d ground::Location() 33 { 34 return (location); 35 } 36 37 inline bool ground::LoadMesh(std::string meshFileName) 38 { 39 return (groundMesh.Load(meshFileName));
Сила тяжести и метательные снаряды 297 40 } 41 42 inline bool ground::Render(void) 43 { 4 4 return (groundMesh.Render()); 45 } В листинге приведено все содержимое файла, поскольку он является частью программы примера, а не частью платформы физического моделирования. Класс ground реализован с использованием платформы, поэтому в файл ground.h включен файл PMFramework.h. Пространство pmframework задействуется в строке 3. В классе ground объявляются элементы данных (строки 8-9 листинга ground.h). Первый элемент данных отслеживает положение поверхности, позволяя размещать ее выше или ниже начала глобальной системы координат. В этой программе поверхность считается горизонтальной, поэтому используется только компонент у вектора location. Элемент groundMesh позволяет программе загружать сетчатую модель и растровый рисунок для текстурирования поверхности. Все методы класса очень просты. Конструктор не делает вообще ничего. Другие методы читают и записывают значения элементов данных. В листинге 11.5 содержатся функции программы, моделирующей силу тяжести. Листинг 11.5. Файл Launcher.cpp 1 #include "PMFramework.h" 2 #include "Ground.h" 3 4 using namespace pmframework; 5 6 #define MILLISECONDS_PER_FRAME 33 7 #define TOTAL_BALLS 5 8 9 rigidjbody allBalls[TOTAL_BALLS]; 10 ground theGround; 11 12 bool TimeToUpdateFrame{ 13 DWORD currentTime); 14 void HandleOverlapping( 15 scalar timeIncrement, 16 int objectl, 17 int object2, 18 collision StheCollision); 19 20 bool OnAppLoadO 21 { 22 window_init_params windowParams;
298 Глава 11 23 windowParams.appWindowTitle = "Gravity Test"; 24 windowParams.defaultX=100; 25 windowParams.defaultY=100; 26 windowParams.defaultHeight=400; 27 windowParams.defaultWidth=400; 28 29 d3d_init_params d3dParams; 30 d3dParams.renderingDeviceClearFlags = D3DCLEAR_TARGET | 31 D3DCLEAR_ZBUFFER; 32 d3dParams.surfaceBackgroundColor = D3DCOLOR_XRGB@,0,255); 33 d3dParams.enableAutoDepthStencil = true; 34 d3dParams.autoDepthStencilFormat = D3DFMT_D16; 35 d3dParams.enableD3DLighting = false; 36 37 theApp.InitApp(windowParams,d3dParams); 38 39 return (true); 40 ) 41 42 bool PreD3DInitialization() 43 { 44 return (true); 45 } 46 47 bool PostD3DInitialization() 48 { 49 return (true); 50 } 51 52 bool GameInitialization() 53 { 54 // Создаем матрицу отображения - как в предыдущих примерах. 55 D3DXVECTOR3 eyePoint@.Of,3.Of,-Ю.Of); 56 D3DXVECTOR3 lookatPoint@.Of,0.Of,0. Of) ; 57 D3DXVECTOR3 upDirection@.Of,1.Of,0.Of); 58 D3DXMATRIXA16 tempViewMatrix; 59 D3DXMatrixLookAtLH( 60 &tempViewMatrix,&eyePoint,&loo]catPoint,SupDirection); 61 theApp.ViewMatrix(tempViewMatrix); 62 63 // Создаем матрицу проецирования. 64 D3DXMATRIXA16 projectionMatrix; 65 D3DXMatrixPerspectiveFovLH( 66 SprojectionMatrix,D3DX_PI/4,1.0f,1.0f,100.Of) ; 67 theApp.ProjectionMatrix(projectionMatrix); 68 69 vector_3d tempVector@.0,0.0,0.0); 70 force theForce; 71
Сила тяжести и метательные снаряды 299 72 /* Загружаем сетчатую модель "шарика". 73 Не забудьте: эта модель поставляется вместе с SDK DirectX. 74 Бе нужно скопировать из папки 75 <SDKDIR>\Samples\C++\Direct3D\Tutorials\Tut06_Meshes, 76 где <SDKDIR> - полный путь к папке, в которую установлен 77 SDK DirectX. Скопируйте файлы tiger.x и tiger.bmp 78 в папку проекта.*/ 79 allBalls[0].LoadMesh("tiger.x"); 80 81 // Задаем начальное местоположение первого шарика. 82 tempVector.SetXYZ(-3.Of,5.0,5.0); 83 allBalls[0].Location(tempVector); 84 85 theForce.Force(vector_3d@.0,-9.8f,0.0)); 86 theForce.ApplicationPoint(tempVector); 87 allBalls[0].ConstantForce(theForce); 88 89 // Задаем вращательную инерцию первого шарика. 90 tempVector.SetXYZC9.6f,39.6f,12.5f); 91 allBalls[0].Rotationallnertia(tempVector); 92 93 // Задаем вектор силы, определяющий линейное движение шарика. 94 tempVector.SetXYZA.0,-1.0,0.Of); 95 theForce.Force(tempVector); 96 97 // Задаем точку, к которой приложена сила. 98 tempVector.SetXYZ@.0,0.0,-1.Of); 99 theForce.ApplicationPoint(tempVector); 100 101 // Сохраняем силу в объекте класса rigidjbody. 102 allBalls[0].ImpulseForce(theForce); 103 104 // Задаем массу шарика. 105 allBalls[0].MassA00); 106 107 // Задаем ограничивающую сферу шарика. 108 allBalls[0].BoundingSphereRadius@.75f); 109 110 // Задаем упругость шарика. 111 allBalls[0].CoefficientOfRestitution@.5f); 112 113 // Копируем характеристики первого шарика во все остальные. 114 allBalls[4] = allBalls[3] = allBalls[2] = allBalls[1] = 115 allBalls[0]; 116 117 /* Задаем другое начальное местоположение для второго 118 шарик». */ 119 tempVector.SetXYZ@.0,3.Of,5.0); 120 allBalls[1].Location(tempVector); 121
300 Глава 11 122 theForce.Force(vector_3d@.0,-9.8f,0.0)); 123 theForce.ApplicationPoint(tempVector); 124 allBalls[1].ConstantForce(theForce); 125 126 // Прикладываем другую силу. 127 tempVector.SetXYZ(-1.0f,-1.0f,0.0): 128 theForce.Force(tempVector); 129 tempVector.SetXYZ@.0,-1.Of,-1.Of); 130 theForce.ApplicationPoint(tempVector); 131 allBalls [1] . ImpulseForce (theForce) ,' 132 133 // Задаем упругость шарика. 134 allBalls[1].CoefficientOfRestitution@.OOlf) ; 135 136 /* Задаем другое начальное местоположение для третьего 137 шарика. */ 138 tempVector.SetXYZD.0,4.Of ,7.0) ; 139 allBalls{23.Location(tempVector); 140 141 theForce.Force(vector_3d@.0,-9.8f,0.0)); 142 theForce.ApplicationPoint(tempVector); 143 allBalls[2].ConstantForce(theForce)> 144 145 // Прикладываем другую силу. 146 tempVector.SetXYZ(-3.0,20.0,0.0); 147 theForce.Force(tempVector); 148 tempVector.SetXYZA.Of,-1.0f,0.0); 149 theForce.ApplicationPoint(tempVector); 150 allBalls [2] . ImpulseForce (theForce) <" 151 152 // Задаем упругость шарика. 153 allBalls[2].CoefficientOfRestitution@.17f); 154 155 // Задаем начальное местоположение• 156 tempVector.SetXYZ@.0,4.Of,-15.Of); 157 allBalls[3].Location(tempVector); 158 159 // Прикладываем силу тяжести. 160 theForce.Force(vector_3d@.0,-9.8f,0.0)); 161 theForce.ApplicationPoint(tempVector); 162 allBalls [3] .ConstantForce (theForce) ,' 163 164 // Прикладываем импульсную силу. 165 tempVector.SetXYZ@.0,30.0,50.0); 166 theForce.Force(tempVector); 167 tempVector.SetXYZ@.0,-1.Of,0.0); 168 theForce.ApplicationPoint(tempVector); 169 allBalls[3].ImpulseForce(theForce): 170
Сила тяжести и метательные снаряды 301 171 // Задаем упругость шарика. 172 allBalls[3].CoefficientOfRestitution@.3f); 173 174 // Задаем начальное местоположение. 175 tempVector.SetXYZ(-10.Of,4.Of,5.Of); 176 allBalls[4].Location(tempVector); 177 178 // Прикладываем силу тяжести. 179 theForce.Force(vector_3d@.0,-9.8f,0.0)); 180 theForce.ApplicationPoint(tempVector); 181 allBalls[4].ConstantForce(theForce); 182 183 // Прикладываем импульсную силу. 184 tempVector.SetXYZA0.0,50.0,0.0); 185 theForce.Force(tempVector); 186 tempVector.SetXYZ@.0,0.Of,1.0); 187 theForce.ApplicationPoint(tempVector); 188 allBalls[4].ImpulseForce(theForce); 189 190 // Задаем упругость шарика. 191 allBalls[4].CoefficientOfRestitution@.6f); 192 193 theGround.LoadMesh("seafloor.x"); 194 195 return (true); 196 } 197 198 bool HandleMessage( 199 HWND hWnd, 200 UINT msg, 201 WPARAM wParam, 202 LPARAM lParam) 203 { 204 return (false); 205 ) 206 207 bool UpdateFrame() 208 { 209 int i; 210 211 scalar timelncrement = 1; 212 213 DWORD currentTime = ::timeGetTime(); 214 if (!TimeToUpdateFrame(currentTime)) 215 return (true); 216
302 Глава 11 217 // Для каждого объекта... 218 for {i=0; i<TOTAL_BALLS-l;i++) 219 { 220 // Ищем столкновения с другими объектами. 221 for (int j=i+l;j<TOTAL_BALLS-l;j++) 222 { 223 // Если произошло столкновение... 224 collision theCollision{ 225 &allBalls[i], 226 &allBalls[j]); 227 collision_status collisionOccurred = 228 theCollision.CollisionOccurred(); 229 switch (collisionOccurred) 230 { 231 case COLLISION_TOUCHING: 232 // Просчитываем столкновение. 233 theCollision.CalculateReactions(); 234 break; 235 236 case COLLISION_OVERLAPPING: 237 // Шарики перекрываются. Отодвигаем их. 238 HandleOverlapping( 239 timelncrement,i,j,theCollision); 240 break; 241 242 case COLLISION_NONE: 243 // Здесь ничего не нужно делать. 244 // Добавлено только для завершенности. 245 break; 246 } 247 } 248 ) 249 250 // 251 // Проверка столкновения с поверхностью земли. 252 // 253 // Для каждого "шарика"... 254 for (i=0;i<TOTAL_BALLS;i++); 255 { 256 /* Находим расстояние между нижним краем 257 ограничивающей сферы и поверхностью земли. */ 258 scalar distance = 259 allBalls[i].Location().Y() - 260 allBalls[i].BoundingSphereRadius() - 261 theGround.Location(). Y(); 262 263 /* Если расстояние меньше радиуса ограничивающей сферы.. */ 264 if ((CloseToZero(distance)) || (distance 0.0)) 265 {
Сила тяжести и метательные снаряды 303 266 /* Моделируем отскок, меняя знак компонента у линейной 267 скорости объекта. Учитываем эластичность объекта, 268 умножая этот компонент на коэффициент восстановления.*/ 269 vector_3d tempVector =allBalls[i].LinearVelocity(); 270 tempVector.Y(-tempVector.Y() * 271 allBalls[i].CoefficientOfRestitution()); 272 allBalls[i].LinearVelocity(tempVector); 273 274 // Убедимся, что объект только соприкасается с 275 // поверхностью. 276 scalar verticalDistance = 277 allBalls[i].BoundingSphereRadius() + 278 theGround.Location().Y(); 279 /* Немного сдвинем его вверх, чтобы он больше не 280 соприкасался с поверхностью земли. */ 281 verticalDistance += allBalls[ 282 i].BoundingSphereRadius() * O.Olf; 283 allBalls[i].Location().Y(verticalDistance); 284 } 285 ) 286 287 // Обновляем информацию о каждом шарике. 288 for (i=0;i<TOTAL_BALLS;i++); 289 { 290 allBalls[i].Update(timelncrement); 291 } 292 293 return (true); 294 ) 295 296 bool RenderFrame{) 297 { 298 // Задаем матрицу отображения, если она изменилась. 299 theApp.D3DRenderingDevice()->SetTransform( 300 D3DTS_VIEW, StheApp.ViewMatrix()); 301 302 // Задаем матрицу проецирования, если она изменилась. 303 theApp.D3DRenderingDevice()->SetTransform( 304 D3DTS_PROJECTION, StheApp.ProjectionMatrix()); 305 306 // Выполняем рендеринг всех шариков. 307 for (int i=0;i<TOTAL_BALLS;i++) 308 { 309 allBalls[i].Render(); 310 } 311 312 theGround.Render(); 313 return (true); 314 } 315
304 Глава 11 316 bool GameCleanupO 317 { 318 return (true); 319 } 320 321 bool TimeToUpdateFrame( 322 DWORD currentTime) 323 { 324 // Эта инициализация выполняется только однажды. 325 static DWORD lastTime=0; 326 327 // Эта инициализация выполняется при каждом вызове 328 // функции. 329 bool updateFrame=false; 330 331 // Если прошло достаточно миллисекунд... 332 if (currentTime-lastTime >= MILLISECONDS_PER_FRAME) 333 { 334 // Пора просчитывать новый кадр. 335 updateFrame=true; 336 337 // Сохраняем время последнего обновления кадра. 338 lastTime=currentTime; 339 } 340 return (updateFrame); 341 } 342 343 void HandleOverlapping( 344 scalar timelncrement, 345 int balll, 346 int ball2, 347 collision StheCollision) 348 { 349 scalar changelnTime = timelncrement; 350 351 // Мы уже знаем, что произошло столкновение с перекрытием. 352 collision_status collisionOccured = 353 COLLISION_OVERLAPPING; 354 355 // Пока не просчитали и инкремент времени не нулевой... 356 for (bool done=false; 357 ('done) && ("CloseToZero(changelnTime)); 358 /* Нет ни инкремента, ни декремента */) 359 { 360 // Проверим тип столкновения. 361 switch (collisionOccured) 362 { 363 // Если ограничивающие сферы все еще перекрываются... 364 case COLLISION_OVERLAPPING: 365 {
Сила тяжести и метательные снаряды 305 366 rigid_body objectl=allBalls[balll]; 367 rigid_body object2=allBalls[Ьа112]; 368 369 // Обращаем направления скоростей и сил. 370 vector_3d tempVector = 371 objectl.AngularVelocity(); 372 tempVector *= -1; 373 objectl.AngularVelocity(tempVector); 374 tempVector = objectl.LinearVelocity(); 375 tempVector *= -1; 376 objectl.LinearVelocity(tempVector); 377 objectl.ImpulseForce().Force( 378 objectl.ImpulseForce().Force() * -1) ; 379 380 // Обращаем направления скоростей и сил. 381 tempVector = 382 object2.AngularVelocity(); 383 tempVector *= -1; 384 object2.AngularVelocity(tempVector); 385 tempVector = object2.LinearVelocity(); 386 tempVector *= -1; 387 object2.LinearVelocity(tempVector); 388 object2.ImpulseForce().Force( 389 object2.ImpulseForce().Force() * -1) ; 390 391 // Выполняем откат по времени. 392 objectl.Update(changelnTime); 393 object2.Update(changelnTime); 394 395 // Уменьшаем шаг по времени. 396 changeInTime/=2; 397 398 // 399 // Готовимся опять двигаться вперед. 400 // 401 402 /* Задаем скорости и сипы для движения вперед*/ 403 tempVector = 404 objectl.AngularVelocity(); 405 tempVector *= -1; 406 objectl.AngularVelocity(tempVector); 407 tempVector = objectl.LinearVelocity(); 408 tempVector *= -1; 409 objectl.LinearVelocity(tempVector); 410 objectl.ImpulseForce().Force( 411 objectl.ImpulseForce().Force() * -1); 412 413 /* Задаем скорости и силы для движения вперед*/ 414 tempVector = 415 object2.AngularVelocity();
306 Глава 11 416 tempVector *= -1; 417 object2.AngularVelocity(tempVector); 418 tempVector = object2.LinearVelocity(); 419 tempVector *= -1; 420 object2.LinearVelocity(tempVector); 421 object2.ImpulseForce().Force( 422 object2.ImpulseForce().Force() * -1) ; 423 424 // Двигаемся вперед на меньшую величину. 425 objectl.Update(changeInTime); 426 object2.Update(changeInTime); 427 428 allBalls[balll] = objectl; 429 allBalls[ball2] = object2; 430 431 // Опять проверяем вид столкновения. 432 collisionOccured = 433 theCollision.CollisionOccurred(); 434 } 435 break; 436 437 // Если ограничивающие сферы теперь соприкасаются.. 438 case COLLISIONJTOUCHING: 439 // Просчитываем столкновение. 440 theCollision.CalculateReactions(); 441 done=true; 442 break; 443 444 // Если столкновения теперь нет... 445 case COLLISION_NONE: 446 // Отступили слишком далеко. Двигаемся вперед. 447 allBalls[balll].Update(changelnTime); 448 allBalls[ball2].Update(changelnTime); 449 // Опять проверяем вид столкновения. 450 collisionOccured = 451 theCollision.CollisionOccurred(); 452 break; 453 } 454 } 455 /* Если цикл завершился, поскольку временной шаг 456 стал почти нулевым... */ 457 if (CloseToZero(changelnTime)) 458 { 459 // Просчитываем столкновение. 460 theCollision.CalculateReactions(); 461 allBalls[balll].Update(changelnTime); 462 allBalls[balll].Update(changelnTime); 463 } 464 }
Сила тяжести и метательные снаряды 307 В листинге 11.5 приведен код из файла Launcher. срр. Этот код весьма напоминает код из файла TigerToss. срр, который мы разбирали в главе 10. Но есть и определенные отличия. Рассмотрим этот код подробнее. Замечание Для экономии бумаги в листинге 11.5 отсутствуют многие комментарии из файла Launcher.срр на компакт-диске (в папке Source\Chapterll\La- uncher). Файл Launcher.срр считает все движущиеся объекты шариками. Однако - в качестве шутки - для их отображения на экране используется та же сетчатая модель тигра, что и в предыдущих главах. В строках 7-9 листинга 11.5 объявляется массив из пяти «шариков». В строке 10 программа объявляет объект класса ground. Если вы посмотрите на функцию GameInitialization() дальше в листинге (она начинается со строки 52), то увидите, что она загружает сетчатые модели и для «шариков», и для поверхности земли. Эта функция задает параметры и начальное местоположение каждого шарика, а также прикладывает к нему импульсную силу. Кроме того, функция GameInitialization() прикладывает к каждому шарику постоянную силу, направленную вертикально вниз, с величиной 9.8. Вспомните - ускорение силы тяжести, обозначаемое вектором g, равно -9.8 м/с2. Оно направлено вертикально вниз, поэтому вектор должен указывать вниз вдоль оси у в нашей системе координат. Для инициализации объекта класса ground нужно только загрузить его сетчатую модель. Функция Gamelnitialization () делает это в строке 193. Функция UpdateFrame (), начинающаяся в строке 207, выполняет солидную часть работы программы. Как и в программе из предыдущей главы, функция UpdateFrame () использует пару вложенных циклов для поиска столкновений между шариками. Операторы, проверяющие шарики на столкновение с поверхностью земли, начинаются в строке 254. Функция UpdateFrame () проверяет, соприкасается ли ограничивающая сфера каждого шарика с поверхностью земли или углубилась в нее. Если сфера углубилась в землю, то функция UpdateFrame () задает для шарика такую высоту, что сфера будет только соприкасаться с поверхностью земли. Это делается в строках 276-278. Но если шарик будет соприкасаться с поверхностью земли, то при следующей итерации моделирования опять будет обнаружено его столкновение с этой поверхностью. Это не то, что нам нужно. Поэтому в строках 281-283 функция UpdateFrame () приподнимает его над поверхностью земли на высоту, равную 1 % от радиуса ограничивающей сферы. Это довольно эффективный прием. Игрок не заметит его, но программа — заметит. В версии функции RenderFrame () есть только одно отличие от версии из предыдущей главы. После рендеринга всех шариков новая версия выполняет рендеринг поверхности земли.
308 Глава 11 Замечание Заметьте, что функция UpdateFrame () теперь не уменьшает до нуля импульсные силы, как это делалось в главе 10. Теперь это делает метод ri- gid_body::Update(). Запустив программу, вы заметите, что шарики (тигры) перемещаются и сталкиваются почти так же, как и в главе 10, но теперь на них действует сила тяжести, и они падают на поверхность земли. Ударяясь об эту поверхность, они отскакивают от нее в соответствии с их коэффициентами восстановления. Если вы не измените коэффициенты восстановления шариков, шарики будут подскакивать все слабее и слабее после каждого столкновения. Постепенно они выкатятся за пределы области видимости. Качение На самом деле в этой программе качение объектов по поверхности не моделируется. Вспомните, программа проверяет, соприкасаются ли шарики с поверхностью земли. Если да, программа немного смещает их, чтобы при следующей итерации моделирования не было обнаружено столкновение. Проще говоря, если объект соприкасается с поверхностью земли, программа предполагает, что этот объект от нее отскакивает. Однако высота отскока будет так мала, что игрок этого отскока просто не заметит. Ему покажется, что шарики просто катятся по поверхности. Так что все в порядке, верно? Ну, это зависит от вашей точки зрения. Если объекты катятся по земле, то эта программа обнаруживает столкновение на каждой итерации моделирования и реагирует на это столкновение. Реакция требует незначительных накладных расходов, и ее можно игнорировать. Однако если вы не хотите игнорировать эту проблему, есть несколько способов ее решения. Первый способ - программный. Если объект катится по горизонтальной поверхности, то его скорость по оси у должна быть практически нулевой. Если скорость объекта близка к 0, то не нужно заставлять его подскакивать. Но этот способ не будет работать, если объект катится по склону, поскольку его скорость по оси у будет отлична от нуля. Второй способ - сочетание программного и физического. Добавьте в класс rigid_body элемент данных, отслеживающий местоположение объекта в предыдущей итерации моделирования, или просто сохраняйте резервную копию объекта, к которой можно будет обратиться на следующей итерации, как в методе HandleOverlapping (). Используя предыдущее местоположение, высоту поверхности и формулу F = mg, вычислите силу соударения. Если сила практически нулевая, значит, объект катится, а не подскакивает - его нужно просто перемещать соответственно его линейной скорости.
Сила тяжести и метательные снаряды 309 Третий способ - более физический, чем программный. Добавьте силу с ускорением 9.8 м/с2 в направлении, перпендикулярном поверхности земли. Эту силу нужно учитывать, просчитывая движение объекта, и объект будет вести себя нужным образом. Но если вам интересно мое мнение, всю проблему можно просто проигнорировать. Компонент у скорости объектов постепенно уменьшится до 0. После этого объекты будут постоянно находиться на одной и той же высоте над поверхностью земли и двигаться только по горизонтали. Это выглядит вполне правдоподобно, и, по-моему, этого достаточно. Предупреждение Предложенные выше решения работают только на горизонтальной поверхности. В главе 15 «Автомобили, корабли и лодки» описывается работа с неровной поверхностью. Итоги Эта глава продемонстрировала, что понимание основ физики позволяет легко увеличивать реалистичность игр. Если программа правильно моделирует воздействие сил на объекты, то добавить в нее новые силы, например, силу тяжести, не составляет труда.
Глава 12 Системы масс и пружин За последние несколько лет реалистичность компьютерной графики резко возросла. Солидная часть этого повышения реалистичности связана с моделированием систем, состоящих из масс и пружин. Физические законы, определяющие поведение таких систем, на первый взгляд кажутся простыми — уравнения, описывающие эти системы, совсем не сложны. Однако системы масс и пружин пользуются дурной славой по части сложности их моделирования. Основная проблема в их моделировании - численная устойчивость. В этой главе мы рассмотрим системы масс и пружин. Вы узнаете, как с помощью этих систем моделировать объекты, широко распространенные в реальном мире, и сделать игры гораздо более привлекательными и реалистичными. Кроме того, вы познакомитесь с основными проблемами, связанными с реализацией систем масс и пружин в программах. Что можно делать с помощью пружин? Пружины обладают на удивление широкой областью применения в компьютерной графике и играх. В прошлом большинство разработчиков воспринимало пружины только как средство моделирования специфичных объектов вроде батутов. Но некоторые более дальновидные разработчики поняли, что у пружин гораздо более широкая потенциальная область применения. Например, одно из ограничений компьютерной графики в прошлом состояло в сложности моделирования гибких объектов, например, ткани и волос. Эта сложность исчезла, когда разработчики поняли, что и ткань, и волосы можно моделировать с помощью пружин. Волосы и прически До недавнего времени и волосы, и прически большинства персонажей в компьютерных играх были жесткими и неподвижными. Если вы посмотрите на волосы персонажей в играх, в которых есть персонажи-люди, то заметите, что волосы не двигаются, не развеваются и не изгибаются, как
Системы масс и пружин 311 это происходит в реальности. Хороший пример этого недостатка - популярная игра The Sims. В ней длинные волосы такие же жесткие, как доска. Если вы хотите, чтобы у персонажей вашей игры были волосы и прически, которые будут реалистично двигаться, то эти волосы и прически нужно представлять в виде систем масс и пружин. Рисунок 12.1 показывает пример такого представления. Внутреннее представление Внешнее представление Рис. 12.1. Прическа «хвостиком», представленная в виде набора масс и пружин На рисунке 12.1 изображены два представления прически «хвостиком». Первое, показанное слева, - это внутреннее представление. Игрок никогда его не видит. На экране изображается только представление справа. Зачем использовать два разных представления? В программе «хвостик» представляется в виде последовательности материальных точек, соединенных гибкими пружинами. Пружина в верхнем конце «хвостика» должна быть намного жестче, чем пружины в нижнем конце, чтобы поведение «хвостика» было реалистичным. При движении голова персонажа будет прикладывать силу к материальной точке в верхнем конце «хвостика». Эта материальная точка передаст усилия на первую пружину. Пружина передаст усилие на следующую материальную точку и приведет ее в движение. Эта последовательность движений будет передаваться дальше по цепочке масс и пружин. Использование такого подхода позволит отслеживать положение каждого сегмента «хвостика». Когда нужно отобразить «хвостик» на экране, игра будет отображать его сегмент за сегментом. Внешнее представление каждого сегмента - это просто сетчатая модель, по форме похожая на сосиску и обтянутая текстурой. Программа должна совмещать расположение каждой модели с расположением соответствующей материальной точки. По мере движения материальных точек будут двигаться и модели. Реалистичность их движения будет зависеть от масс материальных точек и характеристик пружин. Если у вас есть достаточная вычислительная мощность, можно моделировать таким образом поведение каждой пряди волос в прическе.
312 Глава 12 Впрочем, если у вас достаточно вычислительной мощности для этого, вы, вероятно, используете компьютер, произведенный на другой планете. Обычно при моделировании длинных волос они представляются как одно или несколько полотен ткани. Ткань Моделирование ткани похоже на моделирование набора взаимосвязанных «хвостиков». Это иллюстрирует рисунок 12.2. Внутреннее представление Внешнее представление Рис. 12.2. Внутреннее и внешнее представление ткани Как видно из рисунка 12.2, в программе ткань представляется в виде сетки материальных точек. Каждая точка связана с соседними точками по горизонтали, вертикали и диагонали посредством пружин. Пружины придают поведению ткани реалистичность. Внешнее представление ткани — это просто плоская сетчатая модель, обтянутая текстурой с обеих сторон. Положение каждого вертекса модели должно соответствовать положению одной из материальных точек сетки. Реализуя в игре ткань, нужно сделать ячейки сетки гораздо меньшими, чем на рисунке 12.2. Пружины должны быть гораздо мягче, чем в «хвостике». Одно из основных преимуществ использования этого метода для моделирования ткани - материальные точки, образующие ткань, могут взаимодействовать с любыми другими объектами в игре. Для моделирования этого взаимодействия можно использовать рассмотренные в предыдущих главах методы обнаружения столкновений и реагирования на них. Если хорошо реализовать эти методы, то ткань будет оборачиваться вокруг других объектов, развеваться на ветру и вообще вести себя так, как должна вести себя настоящая ткань.
Системы масс и пружин 313 Основа: гармонические колебания Представьте себе массу, подвешенную на пружине к потолку, как на рисунке 12.3. Предположим, что в начале масса не подвешена к пружине. Если затем подвесить массу и отпустить ее, то под действием силы тяжести масса двинется вниз, растягивая пружину. Пружина приложит к массе направленную вверх силу. Под действием этой силы масса двинется вверх. Тянущая ее вверх сила начнет спадать, и сила тяжести опять потащит массу вниз. Такие колебания теоретически будут продолжаться вечно. В реальности они постепенно угаснут из-за неидеальности пружины и других причин, но пока проигнорируем это и будем считать, что колебания продолжаются вечно. Это пример незатухающих гармонических колебаний. .ДУШШИД Рис. 12.3. Масса, подвешенная на пружине к потолку Маятник, показанный на рисунке 12.4, - это еще более простой пример гармонических колебаний. Изучение поведения маятника позволит нам понять физические основы простого гармонического движения. Это понимание, в свою очередь, позволит нам разобраться, как моделировать пружины. Если качнуть маятник, то груз в его конце будет выведен из равновесного состояния. Сила тяжести будет тянуть груз к начальному вертикальному положению. Эта сила выражается таким равенством: F = -mg sine
314 Глава 12 -mg cos © Рис. 12.4. Простой маятник Здесь m — масса груза в маятнике, g - ускорение силы тяжести. Угол 0 — это угол смещения груза относительно вертикали (см. рис. 12.4). Для простых гармонических колебаний длина дуги, по которой движется груз к вертикали, обозначается х. Эту длину можно представить в виде х = 0 • L Здесь L - длина нити, на которой закреплен груз (см. рис. 12.4). Для маленьких углов 0 значение sin 0 приближенно равно 0, поэтому можно преобразовать предыдущее равенство и подставить результат в выражение для силы: F=-mgr Это равенство можно слегка видоизменить: с т8 F= х Если длина нити (L) не изменяется, то величина (mg / L) тоже будет постоянной. Обозначим эту величину к. При этом мы получим формулу F = -кх
Системы масс и пружин 315 Закон Гука Закон Гука - это простое выражение для вычисления силы, с которой пружина стремится вернуть прикрепленную к ней массу в равновесное положение: F = -kx Но это же формула для гармонических колебаний маятника! Мы используем эту формулу, поскольку движение масс, прикрепленных к пружинам, носит гармонический характер. Для пружин к не обязательно равно mg / L. Значение к определяется жесткостью пружины. Чем жестче пружина, тем больше усилие, с которым она тянет массу по направлению к равновесному положению. Поэтому чем жестче пружина, тем больше ее значение к. У мягких пружин значение к невелико, поскольку развиваемые ими усилия малы. Затухающие гармонические колебания В реальном мире массы, подвешенные к пружинам, не будут колебаться вечно. Колебания будут затухать, поскольку пружины неидеальны и часть энергии будет теряться из-за трения. Кроме того, на систему может влиять сопротивление воздуха или воды. На рисунке 12.5 показан пример системы, в которой гармонические колебания будут затухать под действием сопротивления воды. I Жесткость пружины к Масса гл Затухание Ь Рис. 12.5. Затухающие гармонические колебания
316 Глава 12 Груз на рисунке 12.5 подвешен к пружине. Кроме того, к грузу прикреплена пластинка, погруженная в вязкую жидкость. Эта жидкость обуславливает появление тормозящей силы, пропорциональной скорости движения груза, но обратной по направлению к этой скорости. Поэтому тормозящую силу описывает выражение: F = -bv Здесь v - скорость движения груза, a b - коэффициент, связывающий тормозящую силу и эту скорость. Общая сила, действующая на груз, будет такой: F = -kx - bv Теоретически этой простенькой формулы достаточно для моделирования систем масс и пружин. К несчастью, реальность несколько сложнее. Попробуем реализовать в программе фрагмент ткани и разобраться, почему это сложно. Реализация ткани Реализовать в программе кусок ткани довольно сложно. Нужно реализовать всю физику материальных точек, образующих ее, и всю физику пружин, связывающих эти материальные точки. Кроме того, нужно знать, как искривлять и деформировать сетчатые модели в Direct3D, чтобы совмещать вертексы моделей с материальными точками. Деформация моделей требует хорошего знания возможностей Direct3D, и в этой книге она не рассматривается. Мы аппроксимируем внешний вид ткани увеличенными сетчатыми моделями для отдельных материальных точек. Хотя внешний вид ткани, полученной таким образом, будет не слишком правдоподобным, мы сосредоточимся на физике, а не на DirectX. Замечание Чтобы узнать больше о деформациях сетчатых моделей, попробуйте почитать книгу «Special Effects Game Programming with DirectX» (издательство Premier Press). В этой книге есть целая глава, посвященная деформации изображений. Прочитав эту главу, вы поймете, что нужно сделать, чтобы улучшить внешний вид ткани в игре. Усовершенствование материальных точек Прежде чем мы приступим к реализации ткани, нужно обдумать некоторые ее свойства. Например, ткань можно прикрепить к определенным объектам в ЗР-сцене. Хороший пример такой прикрепленной ткани - гобелен на стене замка. Углы гобелена прикреплены к стене и неподвижны, а остальная часть может двигаться и развеваться. Вспомните,
Системы масс и пружин 317 положение каждого вертекса в сетчатой модели ткани определяется положением соответствующей материальной точки. Поэтому нужно найти способ обеспечить неподвижность некоторых материальных точек. Кроме того, чтобы ткань выглядела реалистично, нужно, чтобы материальные точки были невидимыми. На экране должна отображаться сетчатая модель ткани. Поэтому для реализации ткани нам понадобятся материальные точки, с которыми не связаны сетчатые модели. Хотя мы и не будем полностью реализовывать ткань, я создал версию класса ро- int_mass, соответствующую этим требованиям. В программе моделирования ткани класс point_mass унаследован от базового класса с помощью механизма наследования языка C++. Код базового класса приведен в листинге 12.1. Замечание Исходный код программы моделирования ткани находится в папке Sour- ce\Chapterl2\cloth на поставляемом с книгой компакт-диске. Если вы хотите просто увидеть программу в работе, исполняемый файл находится в папке Source\Chapterl2\Bin. Листинг 12.1. Класс point_mass_base 1 class point_mass_base 2 { 3 private: 4 scalar mass; 5 vector_3d centerOfMassLocation; 6 vector_3d linearVelocity; 7 vector_3d linearAcceleration; 8 vector_3d constantForce; 9 vector_3d impulseForce; 10 11 scalar radius; 12 scalar coefficientOfRestitution; 13 14 bool isImmovable; 15 16 public: 17 point_mass_base (); 18 19 void Mass( 20 scalar massValue); 21 scalar Mass(void); 22 23 void Location( 24 vector_3d locationCenterOfMass); 25 vector_3d Location(void); 26
318 Глава 12 27 void LinearVelocity( 28 vector_3d newVelocity); 29 vector_3d LinearVelocity(void); 30 31 void LinearAcceleration( 32 vector_3d newAcceleration); 33 vector_3d LinearAcceleration(void); 34 35 void ConstantForce( 36 vector_3d sumConstantForces); 37 vector_3d ConstantForce(void); 38 39 void ImpulseForce( 40 vector_3d sumlmpulseForces); 41 vector_3d ImpulseForce(void); 42 43 void BoundingSphereRadius( 44 scalar sphereRadius); 45 scalar BoundingSphereRadius(void); 46 47 void Elasticity(scalar elasticity); 48 scalar Elasticity(void); 49 50 void IsImmovable( 51 bool isMassImmovable); 52 bool IsImmovable(void); 53 54 virtual bool Update( 55 scalar changelnTime); 56 }; Класс point_mass_base, определение которого приведено в листинге 12.1, содержит почти всю функциональность, присутствовавшую в классе point_mass. Но в классе point_mass_base нет элемента данных типа mesh и элемента данных для хранения глобальной матрицы. Наконец, нет метода Render (). Эти изменения отражают тот факт, что объекты класса point_mass_base не могут отображаться на экране. Кроме того, заметьте, что метод Update () сделан виртуальным, чтобы его легко было переопределить в производных классах. Далее — теперь можно по отдельности задавать и считывать величины импульсных и постоянно действующих сил, приложенных к объекту. В классе point mass_base есть новый элемент данных — islmmovab- 1е. Если значение этого элемента - true, то соответствующий объект класса point_mass_base не может двигаться. В класс добавлены соответствующие методы для чтения и установки значения элемента islmmovab- 1е. Другие методы класса проверяют значение элемента is Immovable, чтобы выяснить, может ли объект двигаться. В листинге 12.2 приведен код некоторых методов класса point_mass_base, обращающихся к элементу islmmovable.
Системы масс и пружин 319 Листинг 12.2. Применение элемента данных islmmovable 1 inline void point_mass_base: .-LinearVelocity( 2 vector_3d newVelocity) 3 { 4 if {!islmmovable) 5 { 6 linearVelocity = newVelocity; 7 } 8 else 9 { 10 linearVelocity = vector_3d@.0,0.0,0.0); 11 } 12 } 13 14 15 inline void point_mass_base::LinearAcceleration( 16 vector_3d newAcceleration) V> \ 18 if (!islmmovable) 19 { 20 linearAcceleration = newAcceleration; 21 ) 22 else 23 { 24 linearAcceleration = vector_3d@.0,0.0,0.0); 25 } 26 } 27 28 inline void point_mass_base::ConstantForce( 29 vector_3d sumConstantForces) 30 { 31 if (•islmmovable) 32 { 33 constantForce = sumConstantForces; 34 } 35 else 36 { 37 constantForce = vector_3d@.0,0.0,0.0); 38 }; 39 } 40 41 inline void point_mass_base::ImpulseForce( 42 vector_3d sumlmpulseForces) 43 { 44 if (!islmmovable) 45 { 46 ImpulseForce = sumlmpulseForces; 47 } 48 else
320 Глава 12 49 { 50 impulseForce = vector_3d{0.0,0.0,0.0) ; 51 }; 52 } 53 Все методы записи значений в классе проверяют значение элемента islmmovable, чтобы проверить, является ли материальная точка неподвижной. Если да, то эти методы уменьшают до 0 линейную скорость материальной точки, ее линейное ускорение, постоянные и импульсные силы, действующие на нее. В противном случае в элементы записываются значения, передаваемые в качестве параметров этим методам. Если материальная точка неподвижна, ее скорость и ускорение должны быть нулевыми, и на нее не могут действовать никакие силы. Таким способом достигается неподвижность точки - изменить ее местоположение можно, только явным образом задав новые координаты. Если вы хотите, чтобы материальная точка отображалась на экране, не используйте объекты класса point_mass_base. Вместо этого используйте объекты класса point_mass, который в своей новой версии является производным от point_mass_base. Определение новой версии класса point_mass приведено в листинге 12.3. Листинг 12.3. Новая версия класса pointjnass 1 class point_mass : public point_mass_base 2 { 3 private: 4 mesh objectMesh; 5 6 D3DXMATRIX worldMatrix; 7 8 public: 9 bool LoadMesh( 10 std::string meshFileName); 11 12 void ShareMesh( 13 point_mass KsourceMass); 14 15 bool Update( 16 scalar changeInTime); 17 bool Render(void); 18 }; Теперь определение класса point_mass очень короткое. Из листинга 12.3 видно, что этот класс наследует от класса point_mass_base большую часть своей функциональности. В самом классе point_mass теперь всего два элемента данных. Первый предназначен для хранения сетчатой модели. Второй - для хранения глобальной матрицы. Эти два элемента данных позволяют отображать объекты класса на экране.
Системы масс и пружин 321 Поскольку у объектов класса point_mass есть сетчатая модель, которую можно отобразить на экране, в классе point_mass есть метод Load- Mesh (), служащий для загрузки этой модели. Кроме того, в классе есть специальный метод ShareMesh (), позволяющий множеству объектов использовать одну и ту же сетчатую модель. Версия метода Update () класса point_mass переопределяет версию этого метода в классе point_mass_ base. Кроме того, в отличие от класса point__mass_base, в классе ро- int_mass есть метод Render (). Код методов Update () и Render () приведен в листинге 12.4. Листинг 12.4. Методы UpdateQ и Render() класса point_mass 1 bool point_mass::Update( 2 scalar changeInTime) 3 { 4 point_mass_base::Update(changelnTime); 5 6 // Создаем матрицу перемещения. 7 D3DXMatrixTranslation( 8 &worldMatrix, 9 Location().X(), 10 Location().Y(), 11 Location().Z()); 12 13 return(true); 14 } 15 16 bool point_mass::Render(void) 17 { 18 // Сохраняем глобальную матрицу преобразования. 19 D3DXMATRIX saveWorldMatrix; 20 theApp.D3DRenderingDevice()-XSetTransform( 21 D3DTS_WORLD, 22 SsaveWorldMatrix); 23 24 // Применяем глобальную матрицу преобразования 25 //к данному объекту. 26 theApp.D3DRenderingDevice()->SetTransform( 27 D3DTS_WORLD,SworldMatrix); 28 29 // После преобразования выполняем рендеринг объекта. 30 bool renderedOK=objectMesh.Render(); 31 32 // Восстанавливаем глобальную матрицу преобразования. 33 theApp.D3DRenderingDevice()->SetTransform( 34 D3DTS_WORLD, 35 SsaveWorldMatrix); 36 37 return (renderedOK); 38 }
322 Глава 12 Метод Update () класса point_mass теперь делает куда меньше, чем раньше. Он вызывает метод point_mass_base: : Update () в строке 4 листинга 12.4, и вызванный метод выполняет все физические расчеты поступательного движения. По завершении этих расчетов метод point_ mass: : Update () создает матрицу перемещения объекта point_mass по координатам, вычисленным методом point_mass_base: .-Update (). Как и в предыдущих версиях метода Render (), в этой версии глобальная матрица сохраняется во внутренней переменной, а затем Direct3D передается матрица перемещения, хранящаяся в объекте класса point_ mass. Затем выполняется рендеринг сетчатой модели объекта, после чего восстанавливается из внутренней переменной ранее сохраненная глобальная матрица. Наличие классов point_mass_base и point_mass позволяет нам создавать как видимые на экране материальные точки, так и невидимые. Если мы создаем в программах невидимые материальные точки, неплохо было бы дать их классу имя более подходящее, чем point mass base. Поэтому в программу включен оператор typedef, задающий новое имя классу point_mass_base - invisible^point_mass. При этом просто меняется имя класса — его функциональность остается той же. Пружины Чтобы реализовывать в играх волосы и ткань, нам нужно моделировать пружины. В идеале модель пружины должна вести себя так же, как реальные пружины. Программа должна иметь возможность задавать жесткость пружины (к) и коэффициент затухания (Ь) и использовать выведенные ранее в этой главе уравнения поведения пружин, чтобы модели вели себя так же, как реальные пружины. К несчастью, на самом деле моделировать пружины не так просто. Одна из основных сложностей - уравнения поведения пружин не полностью описывают поведение реальных пружин. Эти уравнения подразумевают, что невозможно сжать или растянуть пружину так, как это невозможно в реальности. Если это сделать, реальные пружины ломаются или теряют упругость. При этом материальные точки, прикрепленные к ним, могут двигаться хаотично и непредсказуемо. Если это произойдет в программе, то фрагменты ткани или волос могут двигаться непредсказуемо, часто деформируясь или беспорядочно изменяя размер. Один из способов решения этой проблемы - ввести пределы сжатия и растяжения в модели пружин. Это звучит просто, но на самом деле это довольно сложная задача программирования. Если пружина растянута или сжата до предела, она должна ограничивать перемещения прикрепленных к ней материальных точек, и, соответственно, влиять на сжатие или растяжение других пружин. Вообще, перемещение любой материальной точки в системе масс и пружин приведет к волне изменений положения во всей системе. Конечно, в реальности это так и есть - если мы тянем на себя скатерть, она двигается вся целиком, но смоделировать такое поведение довольно сложно.
Системы масс и пружин 323 Можно задать пределы сжатия и растяжения не в классе пружины - spring. Это нужно делать на уровне системы масс и пружин, то есть в классе ткани - cloth. Подстройки, которые должен выполнять класс cloth, сложны и часто зависят от конкретных ситуаций. Поэтому класс cloth будет малопригоден к широкому применению. Лучше найти более простой подход. Листинг 12.5. Простой класс spring 1 class spring 2 { 3 private: 4 scalar restLength; 5 scalar forceConstant; 6 scalar dampeningFactor; 7 8 point_mass_base *pointMassl; 9 point_mass_base *pointMass2; 10 11 public: 12 spring{); 13 14 void Length( 15 scalar springLength); 16 scalar Length(void); lite void ForceConstant( 19 scalar springForceConstant); 20 scalar ForceConstant(void); 21 22 void DampeningFactor( 23 scalar dampeningConstant); 24 scalar DampeningFactor(void); 25 26 void EndpointMassl( 27 point_mass_base *particlel); 28 point_mass_base *EndpointMassl(void); 29 30 void EndpointMass2( 31 point_mass_base *particle2); 32 point_mass_base *EndpointMass2(void); 33 34 bool IsDisplaced(void); 35 36 void CalculateReactions( 37 scalar changelnTime); 38 };
324 Глава 12 Вместо того чтобы задавать пределы сжатия и растяжения, можно стабилизировать системы масс и пружин, применяя дополнительные силы, гасящие колебания. Эти силы предотвращают хаотичное движение частиц — колебания быстро затухают, и система замирает. Такое решение просто реализовать в программах, и оно не требует больших объемов дополнительных вычислений для использования, поэтому оно широко применяется. Дополнительное затухание вводится на уровне всей системы, то есть в классе cloth, а не spring. Причина этого - в разных типах систем масс и пружин используются разные виды затухания. Сам класс spring не слишком сложен. Определение этого класса приведено в листинге 12.5. В классе spring объявлены пять элементов данных. В первом из них хранится длина пружины в состоянии покоя, то есть не сжатой и не растянутой. В строках 5-6 листинга 12.5 определены элементы данных для хранения характеристик к и b пружин. Кроме того, в классе spring объявлены указатели на материальные точки, к которым крепятся пружины. Обратите внимание — это указатели на тип point_mass_base. Это позволяет нам прикреплять пружины как к видимым, так и к невидимым материальным точкам. Проще говоря, указатели, объявленные в строках 8-9 листинга 12.5, могут указывать на объекты как класса point_mass, так и класса invisible_point_mass. В строках 14-32 определены методы чтения и записи значений в элементы данных. Метод IsDisplaced() определяет, растянута пружина или сжата. Метод CalculateReactions () вычисляет силу, прикладываемую пружиной к материальным точкам, к которым она прикреплена. Код этих двух методов приведен в листинге 12.6. Листинг 12.6. Рабочие методы класса spring 1 bool spring::IsDisplaced(void) 2 { 3 assert(pointMassl!=NULL); 4 assert(pointMass2!=NULL); 5 6 bool IsDisplaced = false; 7 8 vector_3d currentLength; 9 10 /* Находим расстояние между частицами, 11 к которым прикреплена пружина. */ 12 currentLength = 13 pointMassl->Location() - pointMass2->Location(); 14 15 /* Находим разность между длиной пружины в данный момент 16 и ее длиной в состоянии покоя. */ 17 scalar lengthDifference =
Системы масс и пружин 325 18 currentLength.NormSquared() - (restLength*restLength); 19 20 // Если разность заметно отличается от 0... 21 if (!CloseToZero(lengthDifference)) 22 { 23 i sD i sp1aced=true; 24 } 25 26 return (isDisplaced); 27 } 28 29 void spring::CalculateReactions( 30 scalar changelnTime) 31 { 32 assert(pointMassl!=NULL); 33 assert(pointMass2!=NULL); 34 35 vector_3d currentLength; 36 37 // Находим длину пружины в данный момент. 38 currentLength = 39 pointMassl->Location() - pointMass2->Location(); 40 41 // Преобразуем ее в скаляр. 42 scalar currentLengthMagnitude = currentLength.Norm(); 43 44 /* Находим разность между длиной пружины в данный момент 45 и ее длиной в состоянии покоя. */ 46 47 scalar changeInLength=currentLengthMagnitude-restLength; 48 49 // Если изменение длины практически нулевов... 50 if (CloseToZero(changeInLength)) 51 { 52 // Уменьшаем его до 0. 53 changeInLength=0.0; 54 } 55 56 // Находим величину силы, развиваемой пружиной. 57 scalar springForceHagnitude = 58 forceConstant * changelnLength; 59 60 // Находим величину гасящей силы в пружине. 61 scalar dampeningForceMagnitude; 62 if (changelnTime.Of) 63 { 64 dampeningForceMagnitude =
326 Глава 12 65 dampeningFactor * changelnLength * changeInTime; 66 } 67 else 68 { 69 dampeningForceMagnitude = 70 dampeningFactor * changelnLength / changeInTime; 71 } 72 73 // Гасящая сила всегда меньше силы, развиваемой пружиной. 74 if (dampeningForceMagnitude springForceMagnitude) 75 { 76 dampeningForceMagnitude = springForceMagnitude; 77 } 78 79 // Уменьшаем силу, развиваемую пружиной. 80 scalar responseForceMagnitude = 81 springForceMagnitude - dampeningForceMagnitude; 82 83 // Преобразуем силу, развиваемую пружиной, в вектор. 84 vector_3d responseForce = 85 responseForceMagnitude * 86 currentLength.Normalize(SCALAR_TOLERANCE); 87 88 // Прикладываем развиваемую пружиной силу 89 //к материальным точкам. 90 pointMassl->ImpulseForce( 91 pointMassl->ImpulseForce() + -l*responseForce); 92 pointMass2->ImpulseForce( 93 pointMass2->XmpulseForce() + responseForce); 94 } Метод IsDisplaced() находит длину пружины в данный момент, вычитая векторы местоположения двух материальных точек, к которым эта пружина прикреплена. Магнитуда вектора, получающегося в результате вычитания, будет равна расстоянию между этими двумя материальными точками, то есть длине пружины. В строках 17-18 метод IsDisplacedO находит магнитуду или норму вектора. Чтобы найти норму, нужно вычислить квадратный корень. Дабы избежать этого, метод IsDisplacedO использует квадрат нормы. Он вычитает квадрат длины пружины в состоянии покоя из квадрата нормы. Этого достаточно, поскольку на самом деле знать расстояние между двумя материальными точками методу не нужно. Ему нужно только знать, сжата пружина или растянута. Применение квадратов норм вместо норм позволяет это определять. Метод CalculateReactions (), начинающийся в строке 29 листинга 12.6, вычисляет силу, которую прикладывает пружина к материальным
Системы масс и пружин 327 точкам, к которым она прикреплена. Первый шаг, необходимый для вычисления этой силы, - найти длину пружины в данный момент. Это делается в строках 38-39. В отличие от метода IsDisplacedO, метод CalculateReactions () не может обойтись квадратом длины и вынужден вычислять квадратный корень. Замечание Вероятно, вы заметили, что метод, просчитывающий действия пружин, называется CalculateReactions (), а не Update (), как аналогичные по назначению методы других классов в платформе физического моделирования. Это решение основывается на моей личной точке зрения. Поскольку пружина никогда не отображается на экране, и единственное, что она делает - прикладывает силы к другим объектам, я посчитал, что она существенно отличается от других моделируемых объектов, и методу нужно дать другое имя. Далее метод CalculateReactions () вычисляет магнитуду вектора, определяющего длину пружины в данный момент. В строке 47 он находит разность между этой длиной и длиной пружины в состоянии покоя. Эта разность умножается на жесткость пружины в соответствии с формулой F = —кх, где х - изменение длины пружины относительно равновесного состояния. Гасящая сила вычисляется в строках 61-71. Вычисленная величина силы, которую развивает пружина, может оказаться меньше, чем величина гасящей силы. Так ли это, зависит от значений жесткости пружины (к) и коэффициента затухания (Ь). Если коэффициент затухания велик по сравнению с жесткостью пружины, то сила затухания может превзойти силу, развиваемую пружиной. Однако это невозможно с физической точки зрения. Поэтому оператор if в строках 74-77 гарантирует, что гасящая сила не превысит силы, развиваемой пружиной. Это в значительной степени гарантирует устойчивость моделируемой системы масс и пружин. В строках 80-81 метод CalculateReactions () находит итоговую силу, прикладываемую пружиной к материальным точкам, к которым эта пружина прикреплена. В строках 84-86 величина этой силы преобразуется в вектор. Метод CalculateReactions () заканчивается прикладыванием этой силы (в противоположных направлениях) к обеим материальным точкам в качестве импульсной силы. Вот, собственно говоря, и все, что нам понадобится для моделирования пружин в программе. Однако, как уже говорилось выше, придется сделать еще кое-что, чтобы пружины оставались устойчивыми. Класс cloth Создав классы для материальных точек и пружин, можно приступить к созданию систем масс и пружин, например, ткани. На рисунке 12.2 в начале этой главы был показан общий принцип представления тканей
328 Глава 12 в программе. Как вы, вероятно, ожидали, реализация их в коде программы куда сложнее. В листинге 12.7 приведено определение описывающего ткань класса cloth. Листинг 12.7. Определение класса cloth 1 class cloth 2 { 3 // Внутренние типы 4 private: 5 enum cloth_constants 6 { 7 PARTICLES_PER_SQUARE=4, 8 TOP_LEFT_PARTICLE=0, 9 TOP_RIGHT_PARTICLE, 10 BOTTOM_LEFT_PARTICLE, 11 BOTTOM_RIGHT_PARTICLE, 12 TOP_SPRING = 0, 13 BOTTOMJSPRING, 14 RIGHT_SPRING, 15 LEFT_SPRING, 16 TOP_RIGHT_TO_BOTTOM_LEFT_SPRING, 17 TOP_LEFT_TO_BOTTOM_RIGHT_SPRING, 18 SPRINGS_PER_SQUARE=6, 19 }; 20 21 struct index_pair 22 { 23 int row,col; 24 }; 25 26 struct cloth_square 27 { 28 index_pair partxclelndex[PARTICLES_PER_SQUARE]; 29 int springIndex[SPRINGS_PER_SQUARE]; 30 }; 31 32 // Private-элементы данных. 33 private: 34 int totalRows; 35 int totalCols; 36 int totalSprings; 37 point_mass **allParticles; 38 spring *allSprings; 39 cloth_square **allSquares; 40 scalar linearDampeningCoefficient; 41
Системы масс и пружин 329 42 // Private-методы 43 private: 44 void cloth::HandleCollision( 45 vector_3d separationDistance, 46 scalar changeInTime, 47 index_pair firstParticle, 48 index_pair secondParticle); 49 50 // Public-методы 51 public: 52 cloth( 53 int particleRows, 54 int particleCols, 55 scalar particleMass, 56 scalar particleRadius, 57 scalar particleElasticity, 58 scalar spaceBetweenParticles, 59 scalar clothStiffness, 60 scalar dampeningFactor, 61 scalar linearDampeningFactor, 62 vector_3d upLeftCorner); 63 64 void ParticlelmpulseForce( 65 int row,int col,vector_3d impulseForce); 66 vector_3d ParticlelmpulseForce( 67 int row,int col); 68 69 void ParticleConstantForce( 70 int row,int col,vector_3d constantForce); 71 vector_3d ParticleConstantForce( 72 int row,int col); 73 74 void IsParticlelmmovable( 75 int row,int col,boo1 isMassImmovable); 76 bool IsParticlelmmovable( 77 int row,int col); 78 79 bool LoadMesh(std::string meshFileName); 80 bool Update(scalar changelnTime); 81 bool Render(void); 82 }; Функциональность класса cloth обширнее, чем требуется в программе примера из этой главы. Этот класс делит ткань на квадратные участки. Углы каждого такого участка — это четыре материальные точки в системе масс и пружин. Такое представление ткани дает вам ряд преимуществ. Во-первых, можно добавлять собственный код в методы класса cloth, чтобы выполнять сдециальные операции над отдельными
330 Глава 12 участками ткани. Это, вероятно, потребуется вам, если вы будете серьезно работать с тканью в играх. Во-вторых, разделение ткани на квадраты дает возможность применять отдельную сетчатую модель к каждому такому квадрату, а не использовать одну сетчатую модель для всего полотна ткани. В результате можно широко варьировать внешний вид ткани. Чтобы упростить разделение ткани на квадраты, в классе cloth определен ряд внутренних типов и констант. Все константы включены в перечисление cloth_constants, определенное в строках 5-19 листинга 12.7. Эти константы задают количество материальных точек в квадрате (конечно, 4) и позицию каждой материальной точки в квадрате. Кроме того, константы задают количество и расположение пружин, соединяющих материальные точки. Тип index_pair позволяет легко задавать строки и столбцы материальных точек и квадратов. Он используется в типе cloth_square, определенном в строках 26-30 листинга 12.7. Каждый экземпляр типа cloth_square содержит массив элементов типа index_pair (строка 28 листинга). Этот массив хранит номера строк и столбцов для каждой из материальных точек в квадрате. Кроме того, в типе cloth_square определен целочисленный массив. В нем хранятся индексы пружин в квадрате. Если вы посмотрите еще раз на рисунок 12.2, то увидите квадраты, образующие ткань, и заметите, что и материальные точки, и пружины входят в состав нескольких квадратов. Поэтому хранить их в типе cloth_ square бессмысленно — это приведет к их дублированию и напрасной трате памяти. Private-элементы данных класса cloth объявлены в строках 34-40 листинга 12.7. В этих элементах данных хранится количество строк и столбцов в системе, а также общее количество пружин в ней. В строке 37 объявлен указатель на указатели на объекты point_ mass. Его использование позволяет классу cloth динамически выделять под ткань произвольное количество материальных точек. Как вы вскоре увидите, этот указатель используется в конструкторе для динамического выделения двумерного массива. Замечание Не забывайте - класс cloth в программе использует массив объектов класса point_mass, а не invisible_point_mass, поскольку мы не будем связываться с деформацией сетчатых моделей. Вместо этого мы отобразим ткань в виде набора сетчатых моделей материальных точек. Это будет не слишком реалистично, но если вы хотите получить реалистично выглядящую ткань, то должны уметь выполнять деформацию сетчатых моделей и использовать в классе cloth массив объектов класса invisible_po- int mass. Кроме того, в классе cloth есть указатель на пружины. Этот указатель используется для работы с одномерным динамически выделяемым массивом пружин. Поскольку квадраты ткани расположены в определенных
Системы масс и пружин 331 столбцах и строках, то в классе cloth есть указатель на указатели на структуры cloth_square (строка 39). Этот указатель используется для работы с динамически выделяемым двумерным массивом квадратов ткани. В классе cloth есть также ряд методов как доступных извне, так и предназначенных для внутреннего использования. Начнем их рассмотрение с конструктора. Инициализация фрагмента ткани Инициализация фрагмента ткани - довольно сложный процесс. Его выполняет конструктор класса cloth, код которого приведен в листинге 12.8. Листинг 12.8. Конструктор класса cloth 1 cloth::cloth( " 2 int particleRows, 3 int particleCols, 4 scalar particleMass, 5 scalar particleRadius, 6 scalar particleElasticity, 7 scalar spaceBetweenParticles, 8 scalar clothStiffness, 9 scalar dampeningFactor, 10 scalar linearDampeningFactor, 11 vector_3d upLeftCorner) 12 { 13 assert(particleRows=2); 14 assert(particleCols=2); 15 16 linearDampeningCoefficient = linearDampeningFactor; 17 18 // Выделяем память под одно измерение массива 19 // материальных точек. 20 allParticles = new point_mass * [particleRows]; 21 22 // Если память выделить не удалось... 23 if (allParticles==NULL) 24 { 25 // Генерируем исключение. 26 pmlib_error outOfMemory( 27 "Can't allocate memory for cloth."); 28 throw outOfMemory; 29 } 30 31 // 32 // Выделяем память под второе измерение массива.
332 Глава 12 зз // 34 int i,j; 35 // Для каждой строки... 36 for (i=0;KparticleRows;i++) 37 { 38 // Выделяем по материальной точке для каждого столбца 39 // в строке. 40 allParticles[i] = new point_mass [particleCols]; 41 42 // Если материальную точку не удалось выделить... 43 if (allParticles[i]==NULL) 44 { 45 // Генерируем исключение. 46 pmlib_error outOfMemory( 47 "Can't allocate memory for cloth."); 48 throw outOfMemory; 49 } 50 } 51 52 // Находим общее количество пружин в сетке. 53 totalSprings = 54 (particleRows * (particleCols-1)) + 55 ((particleRows-1) * particleCols) + 56 ((particleRows-1) * (particleCols-1) * 2); 57 58 // Выделяем массив. 59 allSprings = new spring [totalSprings]; 60 61 // Если выделить массив не удалось... 62 if (allSprings==NULL) 63 { 64 // Генерируем исключение. 65 pmlib_error outOfMemory( 66 "Can't allocate memory for cloth."); 67 throw outOfMemory; 68 } 69 70 // Выделяем строки под массив квадратов. 71 allSquares = new cloth_square * [particleRows-1]; 72 73 // Если не удалось выделить... 74 if (allSquares=NULL) 75 { 76 // Генерируем исключение. 77 pmlib_error outOfMemory( 78 "Can't allocate memory for cloth."); 79 throw outOfMemory;
Системы масс и пружин 333 80 } 81 //В противном случае массив создан успешно... 82 else 83 { 84 // Для каждой строки в массиве. 85 for (i=0; i<particleRows-l; i++) 86 { 87 // Выделяем квадраты для каждого столбца в строке. 88 allSquares[i] = new cloth_square[particleCols-1]; 89 90 // Если квадраты выделить не удалось... 91 if (allSquares[i]=NULL) 92 { 93 // Генерируем исключение. 94 pmlib_error outOfMemory( 95 "Can't allocate memory for cloth."); 96 throw outOfMemory; 97 } 98 } 99 } 100 101 vector_3d location=upLeftCorner; 102 103 // Задаем характеристики каждой материальной точки. 104 for (i=0; KparticleRows; i++) 105 { 106 for (j=0; j<particleCols; j++) 107 { 108 allParticles[i][j].Mass(particleMass); 109 allParticles[i][j]. 110 BoundingSphereRadius(particleRadius); 111 allParticles[i][j].Elasticity(particleElasticity); 112 allParticles[i][j].Location(location); 113 location.X( 114 location.X()+spaceBetweenParticles)! 115 } 116 location.X( 117 upLeftCorner.X()); 118 location.Y( 119 location.Y() - spaceBetweenPartides) ; 120 ) 121 122 // 123 /* В каждом квадрате подсоединяем горизонтальные пружины в 124 верхней и нижней парах материальных точек. */ 125 // 126 index_pair templndex;
334 Глава 12 127 int currentSpring; 128 for (i=0,currentSpring=O; KparticleRows; i++) 129 { 130 for (j=0;j<particleCols-l;j++, eurrentSpring++) 131 { 132 // Верхняя пара 133 allSprings[currentSpring].EndpointMassl( 134 SallParticles[i][ j]) ; 135 allSprings[currentSpring].EndpointMass2( 136 SallParticles[i][j+1]); 137 138 // Если это не последняя строка... 139 if (i<particleRows-l) 140 { 141 // Сохраняем индекс верхней пружины 142 allSquares[i][j].springlndex[TOP_SPRING] = 143 cur rentSpr ing; 144 145 // Сохраняем индексы материальных 146 // точек, которые она соединяет. 147 templndex.row = i; 148 templndex.col = j; 149 allSquares[i][j]. 150 particleIndex[TOP_LEFT_PARTICLE] = 151 templndex; 152 templndex.col = j+1; 153 allSquares[i][j]. 154 particleIndex[TOP_RIGHT_PARTICLE] = 155 tempIndex; 156 } 157 158 // Если это не первая строка... 159 if (i>0) 160 { 161 /* Эта пружина уже подсоединена, сохраняем ее 162 как нижнюю пружину */ 163 allSquares[i-l][j].springlndex[BOTTOM_SPRING] 164 currentSpring; 165 166 // Сохраняем индексы двух материальных точек, 167 // которые она соединяет. 168 templndex.row = i; 169 templndex.col = j; 170 allSquares[i-l][j]. 171 particleIndex[BOTTOM_LEFT_PARTICLE] = 172 templndex; 173 templndex.col = j+1;
Системы масс и пружин 335 174 allSquares[i-l][j]. 175 particlelndex[BOTTOM_RIGHT_PARTICLE] 176 = templndex; 177 } 178 } 179 } 180 181 // 182 /* В каждом квадрате подсоединяем вертикальные пружины в 183 левой и правой парах материальных точек. */ 184 // 185 for (i=0; KparticleRows - 1; i++) 186 { 187 for (j=0; j<particleCols; j++, currentSpring++) 188 { 189 // Левая пара 190 allSprings[currentSpring].EndpointMassl( 191 iallParticlesfi][j]); 192 allSprings[currentSpring].EndpointMass2( 193 SallParticles[i+1][j]) ; 194 195 // Если это не последний столбец материальных точек... 196 if (j < particleCols - 1) 197 { 198 // Сохраняем индекс левой пружины 199 allSquares[i][j].springlndex[LEFT_SPRING] = 200 currentSpring; 201 202 // Сохраняем индексы материальных точек, 203 // которые она соединяет. 204 templndex.row = i; 205 templndex.col = j; 206 allSquares[i][j]. 207 particlelndex[TOP_LEFT_PARTICLE] = 208 templndex; 209 templndex.row = i+1; 210 allSquares[i][j]. 211 particlelndex[BOTTOM_LEFT_PARTICLE] = 212 templndex; 213 } 214 215 // Если это не первый столбец материальных точек... 216 if (j>0) 217 { 218 // Сохраняем индекс правой пружины 219 allSquares[i][j-1].springlndex[RIGHT_SPRING] 220 = currentSpring; 221
336 Глава 12 222 // Сохраняем индексы материальных 223 // точек, которые она соединяет. 224 tempIndex.row = i; 225 templndex.col = j; 226 allSguares[i][j-1]. 227 particleIndex[TOP_RIGHT_PARTICLE] = 228 templndex; 229 templndex.row = i+1; 230 allSquares[i][j-1]. 231 particlelndex[BOTTOM_RIGHT_PARTICLE] 232 = templndex; 233 } 234 ) 235 } 236 237 // 238 // Подсоединяем диагональные пружины в каждом квадрате. 239 // 240 for (i=0; i<particleRows-l; i++) 241 { 242 for (j=0; j<particleCols-l; j++) 243 { 244 /* Подсоединяем пружину между левой верхней 245 и правой нижней материальными точками. */ 246 allSprings[currentSpring].EndpointMassl( 247 SallParticles[i][j]) ; 248 allSprings[currentSpring].EndpointMass2( 249 SallParticles[i+1][j+1]); 250 allSquares[i][j]. 251 springlndex[TOP_RIGHT_TO_BOTTOM_LEFT_SPRING] 252 currentSpring++; 253 254 /* Подсоединяем пружину между левой верхней 255 и правой нижней материальными точками. */ 256 allSprings[currentSpring].EndpointMassl( 257 SallParticles[i][j+1]); 258 allSprings[currentSpring].EndpointMass2( 259 SallParticles[i+1][j]); 260 allSquares[i][j]. 261 springlndex[TOP_LEFT_TO_BOTTOM_RIGHT_SPRING] 262 currentSpring++; 263 } 264 } 265 266 // Задаем общие характеристики всех пружин. 267 for (i=0; i<totalSprings; i++) 268 {
Системы масс и пружин 337 269 allSprings[i].DampeningFactor(dampeningFactor); 270 allSprings[i].ForceConstant(clothStiffness); 271 vector_3d tempVector = 272 allSprings[i].EndpointMassl()->Location() - 273 allSprings[i].EndpointMass2()->Location(); 274 allSprings[i].Length(tempVector.Norm()); 275 } 276 277 totalRows = particleRows; 278 totalCols = particleCols; 279 } Неплохой конструктор, правда? Он начинается с сохранения коэффициента линейного затухания в строке 16. Вспомните - ранее в этой главе я говорил, что ткань можно сделать более устойчивой, если ввести в нее дополнительное затухание. Именно для этого и предназначен коэффициент линейного затухания. Далее конструктор выделяет массив указателей на объекты класса point_mass. Если массив выделить не удается, конструктор генерирует исключение в строках 22-28. В противном случае конструктор создает материальные точки для каждой строки по очереди. Если строку не удается выделить, генерируется исключение D2-49). Предупреждение Конструктор должен был бы удалять массив указателей, выделенный в строке 20, если в строке 48 генерируется исключение. Если массив не удалить, может возникнуть утечка памяти. В данной версии конструктора массив не удаляется, поскольку вероятность нехватки памяти в примере невелика, а конструктор и так весьма объемен. В строках 53-56 конструктор вычисляет общее количество пружин, которое необходимо для создания сетки. Это количество используется при выделении массива пружин в строке 59. Опять-таки конструктор сгенерирует исключение в строке 67, если не удается выделить память под массив. Далее конструктор выделяет массив указателей на квадраты. Если массив успешно выделен, конструктор выделяет строки квадратов в строках (строки 85-98 листинга 12.8). Начиная со строки 104, конструктор инициализирует характеристики каждой материальной точки, пружины и квадрата ткани. В строках 104-120 с помощью пары вложенных циклов for задаются общие свойства всех материальных точек. Кроме того, эти циклы задают позиции точек в сетке. Конструктор использует очень простой подход к инициализации позиций материальных точек. Он предполагает, что ткань висит вертикально. Если ткань должна занимать какое-то другое положение, координаты точек придется задавать после вызова конструктора.
338 Глава 12 Теперь можно приступить к подсоединению пружин. Поскольку эта задача не очень проста, она разделена на несколько шагов. Сначала конструктор с помощью пары вложенных циклов for (начиная со строки 128) подсоединяет верхнюю пружину каждого квадрата. Он сохраняет индекс пружины и индексы соединяемых этой пружиной частиц. Если пружина находится не в первой строке частиц, она будет одновременно и верхней пружиной текущего квадрата, и нижней пружиной соседнего сверху квадрата. Поэтому строки 159-178 подсоединяют ее, как верхнюю пружину соответствующего квадрата. После завершения выполнения этих циклов все горизонтальные пружины будут подсоединены. Начиная со строки 185, аналогичным образом подсоединяются вертикальные пружины. Пара циклов, начинающаяся в строке 240, выполняет подсоединение диагональных пружин. Затем конструктор задает общие характеристики всех пружин в системе и сохраняет общее количество строк и столбцов материальных точек в системе. Теперь мы готовы увидеть класс cloth в работе. Обновление и рендеринг фрагмента ткани Чтобы ткань вела себя реалистично, все материальные точки и пружины в объекте класса cloth должны реагировать на приложенные к ним силы. Материальные точки должны отскакивать друг от друга при столкновениях. Хотя они не могут быть очень упругими, коэффициент упругости не должен быть нулевым. Если он будет нулевым, материальные точки склеятся — настоящая ткань так себя не ведет. Когда материальные точки сталкиваются между собой, они должны передавать возникающие при столкновениях силы по пружинам. Пружины передадут эти силы другим точкам. Как видите, в ткани происходит множество действий. Нетрудно понять, почему система масс и пружин легко может стать неустойчивой. Дабы предотвратить потерю устойчивости, метод cloth: :Update () должен не только учитывать все силы, возникающие при столкновениях и перемещениях, но и гасить движения материальных точек. Как это делается, показано в листинге 12.9. Листинг 12.9. Метод cloth::Update() 1 bool cloth::Update( 2 scalar changeInTime) 3 i 4 int i,j; 5 6 // Вычисляем силы, возникающие в каждой пружине. 7 for (i=0;i<totalSprings;i++) 8 {
Системы масс и пружин 339 9 if (allSprings[i].IsDisplaced()) 10 { 11 allSprings[i].CalculateReactions(changelnTime); 12 } 13 } 14 15 // Обновляем местоположение каждой материальной точки в сетке. 16 for (i=0;i<totalRows,i++) 17 { 18 for (j=0;j<totalCols;j++) 19 { 20 // 21 /* Проверяем, есть ли столкновения между данной 22 материальной точкой и оставшимися. Проверять, есть 23 ли столкновения с предыдущими точками, незачем. */ 24 // 25 26 for (int k=i;k<totalCols;k++) 27 { 28 for (int m=j+l;m<totalCols;m++) 29 { 30 // Находим вектор расстояния между точками. 31 vector_3d distance = 32 allParticles[i][j].Location() - 33 allParticles[k][m].Location(); 34 scalar distanceSquared = distance.NormSquared(); 35 36 // Находим квадрат суммы радиусов шариков. 37 scalar minDistanceSquared = 38 allParticles[i][j].BoundingSphereRadius() + 39 allParticles[k][m].BoundingSphereRadius(); 40 minDistanceSquared *= minDistanceSquared; 41 42 // Если произошло столкновение... 43 if (distanceSquared < minDistanceSquared) 44 { 45 index_pair firstParticle,secondParticle; 46 firstParticle.row=i; 47 firstParticle.col=j; 48 secondParticle.row=k; 49 secondParticle.col=m; 50 51 // Обрабатываем его. 52 HandleCollision( 53 distance,changelnTime, 54 firstParticle,secondParticle); 55 }
340 Глава 12 56 } 57 } 58 } 59 } 60 61 // 62 /* Строго говоря, это мошенничество. Системы масс и пружин 63 сложно моделировать. Чтобы ткань вела себя реалистично, мы 64 подавим движение материальных точек, входящих в систему.4/ 65 // 66 vector_3d dampening; 67 for (i=0;i<totalRows;i++) 68 { 69 for (j=0;j<totalCols;j++) 70 { 71 dampening = 72 -linearDampeningCoefficient * 73 allPartides [i] [j] .LinearVelocity () ; 74 allParticles[i][j].LinearVelocity( 75 allPar tides [i] [j] .LinearVelocity () + 76 dampening); 77 } 78 } 79 80 // Обновляем данные о каждом шарике. 81 for (i=0;i<totalRows; i++) 82 { 83 for <j=0;j<totalCols; j++) 84 { 85 allParticles[i][j].Update(changeInTime); 86 f 87 } 88 89 return (true); 90 } Из листинга 12.9 видно, что сначала метод Update () просчитывает реакцию пружин в системе на перемещения отдельных материальных точек. Необходимые операции выполняет цикл в строках 7-13. Далее метод Update () отрабатывает пару вложенных циклов for, выполняющих обнаружение столкновений между частицами. Эти циклы похожи на те, что использовались ранее для обнаружения столкновений и реагирования на них. Если вы забыли, как они работают, вернитесь к главе 8. Комментарий в строках 62-64 говорит, что мы мошенничаем в следующем фрагменте кода. Это значит, что мы не точно моделируем поведение настоящих пружин. Как уже говорилось в этой главе, чтобы точно
Системы масс и пружин 341 моделировать их поведение, нам бы пришлось ввести пределы растяжения и сжатия. И, тем не менее, код в строках 67-78 выполняет физическое моделирование — он уменьшает линейные скорости материальных точек в системе соответственно коэффициенту линейного затухания. Значение этого коэффициента должно быть между 0.0 и 1.0. Собственно говоря, здесь мы приближенно моделируем трение. Введя в систему трение и назвав его «линейным затуханием», мы гарантируем, что движение материальных точек не станет слишком быстрым и хаотичным. Подсказка Гашение скорости материальных точек в системах масс и пружин дает хорошие результаты при моделировании ткани. Но этот прием не универсален. В некоторых случаях при моделировании систем масс и пружин лучше гасить линейное ускорение. Силы, действующие на материальные точки, обычно гасить не стоит - при этом они будут не стабилизироваться, а просто медленнее двигаться. После того, как все силы просчитаны и скорости материальных точек найдены, метод Update () вызывает метод point_mass: : Update (), чтобы обновить местоположение каждой материальной точки в системе. Как вы, вероятно, уже поняли, объект класса cloth обновляется при каждом вызове функции UpdateFrame (). Прежде чем мы двинемся дальше, взгляните на листинг 12.10. Эта функция весьма прямолинейна. Каждые 50 кадров она создает импульсную силу случайной величины и прикладывает ее к каждой материальной точке в строке. Результат немного напоминает трепет ткани на ветру. Листинг 12.10. Функция UpdateFrame() 1 bool UpdateFrame() 2 { 3 // Пора обновлять кадр? 4 DWORD currentTime = ::timeGetTime(); 5 if ('TimeToUpdateFrame(currentTime)) 6 return (true); 7 8 // После прохождения определенного числа кадров... 9 static scalar frameCount = 0; 10 static int currentRow=0; 11 if (frameCount>=CLOTH_PARTICLE_ROWS*10) 12 i 13 // 14 // Прикладываем силу к строке материальных точек.
342 Глава 12 15 // 16 vector_3d impulse; 17 18 // Задаем случайные значения х, у и z. 19 impulse.X(RandomScalarB0)) ; 20 impulse.4(RandomScalarB0)); 21 impulse.Z(RandomScalarB0)); 22 23 // Прикладываем вектор силы к каждой материальной 24 // точке в строке. 25 for (int i=0;i<CLOTH_PARTICLE_COLS;i++) 26 { 27 theCloth.ParticlelmpulseForce( 28 currentRow++, 29 i, 30 impulse); 31 } 32 33 /* Если текущая строка - последняя или расположена 34 еще дальше... */ 35 if (currentRow>=CLOTH_PARTICLE_ROWS) 36 { 37 // Сбрасываем строку в 0. 38 currentRow=0; 39 } 40 frameCount=0; 41 } 42 else 43 { 44 frameCount++; 45 ) 4 6 theCloth.Update(STEP_SIZE); 47 return (true); 48 } Рендеринг ткани выполнять еще проще. Его делает метод cloth: : Render (), код которого приведен в листинге 12.11. Этот метод вызывает для каждой материальной точки метод point_mass: : Render (). Листинг 12.11. Метод cloth::Render() 1 bool cloth::Render(void) 2 { 3 bool renderOK=true; 4
Системы масс и пружин 343 5 for (int i=0; (renderOK) SS (KtotalRows) ;i++) 6 { 7 for (int j=0;(renderOK) && {j<totalCols);j++) 8 { 9 renderOK = allParticles[i][j].Render(); 10 } 11 } 12 return (renderOK); 13 } Подстройка ткани Чтобы объект класса cloth вел себя, как ткань, нужно подобрать правильные значения параметров. Каждой материальной точке нужно задать маленькие массу и радиус. Коэффициент восстановления должен быть, вероятно, не более 0.01. Пружины должны быть довольно жесткими, поэтому значение к должно быть довольно большим. Помогает моделированию и затухание. Значение b в примере равно 1000. В вашей игре оно, возможно, будет не таким высоким, но не думаю, что оно будет меньше 500, разве что массы будут очень маленькими. Чтобы подобрать правильное значение коэффициента линейного затухания, нужно экспериментировать. Но обычно оно имеет тот же порядок величины, что и масса. Возможные улучшения для моделирования ткани Класс cloth можно улучшить во множестве аспектов, если вы собираетесь использовать его в играх. Во-первых, можно добавить деформацию сетчатой модели. Для этого придется добавить в класс элемент типа mesh. В методе cloth: : Update () придется реализовать деформацию модели, чтобы совмещать ее вертексы с материальными точками в сетке ткани. Метод cloth: :Render() должен вызывать метод mesh: lender () для элемента типа mesh, который вы добавите в класс cloth. Это упростит код метода cloth: : Render (). И, наконец, нужно заменить в файлах PMCloth.h и PMCloth.cpp все упоминания класса point_mass на invisible_point_mass. Кроме того, стоит добавить обнаружение столкновений материальных точек ткани с другими объектами в игре. Пока в объектах класса cloth обнаруживаются только столкновения между материальными точками самой ткани. Это не слишком хорошо - если ткань столкнется с другим объектом, она просто пройдет сквозь него. Добавить обнаружение столкновений и реакцию на них несложно - для этого нужно просто скопировать и соответствующим образом адаптировать код из главы 8 «Столкновения материальных точек».
344 Глава 12 Сетка в программе примера состоит из 5 столбцов и 5 строк. В играх вам придется использовать куда большие сетки с ячейками гораздо меньших размеров. Можно добавить в ткань затухание движения по квадратам. Проще говоря, можно сделать так, что затухание будет больше в квадратах, в которых одна или несколько материальных точек неподвижны или соприкасаются с другим объектом. Квадраты в классе cloth уже есть, так что реализовать такое затухание будет не слишком сложно. Итоги У систем, состоящих из связанных пружинами масс, на удивление широкая область применения. Теперь, познакомившись с основами моделирования пружин и систем масс и пружин, вы знаете почти все, что нужно знать для моделирования волос, ткани и других гибких объектов.
Глава 13 Вода и волны В этой главе мы воспользуемся инструментами, созданными в главе 12 для моделирования систем масс и пружин. С помощью этих инструментов попытаемся смоделировать воду. Несколько методов ее моделирования мы рассмотрим в этой главе. Вы вправе спросить - зачем моделировать воду несколькими способами? Если кратко, то моделирование воды может оказаться самой сложной задачей в игре. Физика воды - на удивление сложная область. Но не пугайтесь. Есть весьма прямолинейные методы моделирования воды и поведения находящихся в ней объектов. Использование этих методов позволит нам существенно упростить наши программы. Эти методы мы и рассмотрим в данной главе. Подсказка Кстати говоря, многое из того, что будет изложено в этой главе, можно применить и для моделирования воздуха. Собственно говоря, в первом приближении воздух можно считать жидкостью очень малой плотности. Поэтому, если вам нужно моделировать аэростаты и воздушные шары, плавающие в воздухе, можете использовать для этого методы, которые мы рассмотрим в этой главе. Вода и плавучесть Начнем нашу дискуссию с изучения основных свойств воды. Это поможет понять нам, что такое Ълавучесть. Замечание Вода - это удивительное вещество. По всем законам она не должна была бы существовать в том виде, в каком мы ее знаем. Если вы посмотрите на периодическую таблицу химических элементов, то заметите, что все элементы в столбце ниже кислорода очень ядовиты. Элемент ниже кислорода - это сера, S. H2S - это сероводород, газ, слабая кислота. Это же относится и к H2Se, Н2Те и Н2Ро. Поэтому можно было бы предположить, что и Н20 будет кислотой. Но это не так. Причина этой аномалии в том, что угол между связями в молекуле воды равен 105°. Этот необычный угол делает воду замечательным веществом.
346 Глава 13 Свойства воды Вода состоит из огромного множества молекул, непрерывно перемещающихся внутри некоторой емкости. Даже океан — это, в конце концов, всего лишь большая емкость с водой. Представьте себе молекулу воды, находящуюся в 1000 футах (или, если предпочитаете, в 1000 метрах) под поверхностью океана. Над ней находится множество других молекул воды. Все эти молекулы тянет вниз сила гравитации, и они давят на нашу молекулу. Хотя это может показаться очевидным, пожалуйста, продолжайте читать. Все эти рассуждения необходимы. И вот почему. Вода считается несжимаемой жидкостью. Можно сдавить газ, например, кислород, в небольшой объем, но с водой это проделать невозможно. Если мы сжимаем кислород, помещая его в небольшую емкость, то мы повышаем плотность кислорода. Проще говоря, мы помещаем в емкость все больше и больше молекул. Плотность воды - величина практически неизменная. Почти невозможно поместить дополнительное количество молекул воды в емкость, уже заполненную водой. Если мы помещаем воду в сосуд, вода давит на стенки этого сосуда. Это давление прикладывает силу к стенкам сосуда. На рисунке 13.1 показан кубический сосуд с водой. Вода давит на все стенки сосуда с одинаковой силой, и усилие, прикладываемое этим давлением к любой стенке, пропорционально площади этой стенки. Поэтому формула проста: F = pS / 7 Рис. 13.1. Кубический сосуд с водой В этой формуле и F, и S - векторы. На рисунке 13.1 показано, что S - нормальный вектор поверхности стенки. У всех шести сторон нашего сосуда есть нормальные векторы, направленные перпендикулярно к поверхностям этих сторон. На рисунке 13.1 показан нормальный вектор правой стенки сосуда. Величина этого вектора равна площади правой стороны. Для простого кубического сосуда площади всех сторон равны, и каждая сторона - простой квадрат, поэтому площадь стороны и величину нормального вектора найти несложно. Для этого нужно просто возвести в квадрат длину ребра куба. А как насчет сосудов с изогнутыми стенками, например, таких, как на рисунке 13.2?
Вода и волны 347 Рис. 13.2. Сосуд с изогнутыми стенками Если нужно вычислить силу, прикладываемую к стенкам таких сосудов, то изогнутые стенки приходится аппроксимировать множеством плоских маленьких квадратиков. Для каждого такого квадратика мы находим площадь и величину нормального вектора, а затем - действующую на него силу. Потом суммируем силы, действующие на все квадратики, и получаем общую силу. Такой метод хорош, если мы пишем программу для компьютера, поскольку мы все равно аппроксимируем изогнутые поверхности множеством плоских квадратиков. Чтобы найти нормальный вектор к квадратику сетчатой модели, нужно просто просуммировать нормали четырех его вершин. Ну, хорошо, мы разобрались, как найти силу, прикладываемую к стенке сосуда. И что? Зачем нам это нужно в играх? Вода прикладывает силы не только к стенкам сосудов, в которых она находится. Она прикладывает силы и к объектам, находящимся в ней. Если мы разберемся с этими силами, мы поймем, почему некоторые объекты плавают, а некоторые - тонут. Почему объекты плавают Большинство людей знает ответ на этот вопрос. Объект плавает, если вес воды, которая помещается в его объеме, больше, чем вес этого объекта. Но давайте разберемся, почему это так. Чтобы понять, будет ли объект плавать в воде, сначала нужно определить общее усилие, которое вода приложит к нему. Чтобы сделать это, для начала посмотрите на рисунок 13.3. На рисунке 13.3 изображен погруженный в воду сферический объект. Горизонтальная линия делит этот объект пополам. Стрелки показывают усилия, которые прикладывает к объекту давящая на него вода. Мы знаем, что чем глубже погрузится объект, тем сильнее вода будет давить на него. Это значит, что силы, действующие на нижнюю половину объекта, больше сил, действующих на его верхнюю половину. Если сложить все силы, то суммарная сила будет толкать объект вверх, - это выталкивающая сила или сила плавучести.
348 Глава 13 Рис. 13.3. Сферический объект в воде Предположим, что мы знаем величину выталкивающей силы для сферы с рисунка 13.3. Достаточно ли этого, чтобы определить, будет ли сфера плавать? Нет. На сферу действует еще одна сила. Как вы, вероятно, догадались, это сила тяжести. На рисунке 13.4 изображены выталкивающая сила и сила тяжести, действующие на погруженную в воду сферу. Рис. 13.4. Будет ли сфера плавать9 На рисунке 13.4 сумма всех сил с рисунка 13.3 обозначена как Fb - выталкивающая сила. F на рисунке 13.4 - это сила тяжести. Эта сила направлена вниз. Если она больше, чем направленная вверх выталкивающая сила, сфера будет тонуть в воде. Если силы равны, то сфера не будет ни тонуть, ни плавать. Если Fb больше, чем F , сфера будет плавать. А теперь давайте представим, что наша сфера сделана из стали. По опыту мы знаем, что такая сфера потонет. Но как же тогда стальные корабли? На рисунке 13.5 показана стальная сфера с рисунка 13.4 и еще одна стальная сфера - в разрезе. Сфера слева весит столько же, сколько и сфера справа. На их изготовление пошло одинаковое количество стали, но сфера слева пустотелая. Поскольку мы изготовили из стали пустотелую оболочку, площадь ее поверхности гораздо больше, чем площадь поверхности правой сферы. Вспомните - формула для выталкивающей силы
Вода и волны 349 выглядит как F = pS. Увеличивая площадь поверхности, мы увеличиваем S в формуле для выталкивающей силы. На рисунке 13.5 F одинакова для обеих сфер, поскольку их масса одинакова. Но Fb больше для сферы слева, чем для сферы справа. В результате приходим к выводу: левая сфера будет плавать.1 Рис. 13.5. Две сферы с одинаковой массой Давление и плотность Пока у нас есть простая формула F = pS. По этой формуле можно рассчитать, будут ли плавать объекты в наших играх. Замечательно. Но есть одна проблема. Чтобы воспользоваться этой формулой, нужно знать давление воды на заданной глубине. Давайте разберемся, как его вычислить. На рисунке 13.6 изображен сосуд с водой. В воде плавает сфера. На этот раз сила тяжести и выталкивающая сила, действующие на нее, равны. Сфера и не всплывает, и не тонет. Общая глубина воды в сосуде обозначена У}. Обозначим расстояние от сферы до дна сосуда у2. Тогда расстояние от сферы до поверхности будет равно yj — у2. У верхней кромки воды давление равно р0. Давление на глубине h, на которой находится сфера, равно ph. Используя эти обозначения, мы можем находить давление на любой глубине по следующей формуле: Ph = РО + P£h 1 Вывод о пропорциональности выталкивающей силы и площади поверхности тел справедлив только для частного случая тел с разной площадью поверхности, но одинаковой формой (в частности, двух сфер). Если форма тел будет разной, то выталкивающая сила будет пропорциональна объему тела, а не площади его поверхности (закон Архимеда). — (Прим. перев ).
350 Глава 13 Рис. 13.6. сферу Давление на погруженную Эта формула показывает, что давление ph на глубине h равно давлению на поверхности жидкости плюс плотность этой жидкости р умноженная на глубину h и ускорение силы тяжести g. В играх можно предполагать, что давление на поверхности жидкости есть атмосферное давление A атмосфера), поэтому давайте вместо р0 писать pA. А теперь подставим формулу для давления в формулу для выталкивающей силы: гь = (Ра + Pifh)S Следующий вопрос - а какая же у воды плотность? Ответ на него содержится в таблице 13.1. Таблица 13.1. Плотности воды в кг/м3 Условия Плотность 0° С при 1 0 атм 100° С при 1 0 атм 0° С при 50 атм 1000 958 1002 Как видите, плотность воды изменяется незначительно. Поскольку в морской воде растворены соль и другие вещества, ее плотность - около 1.03 г/см3. Заметьте, что мы перешли от кг/м3 к г/см3. Именно граммы на кубический сантиметр - часто используемая в программах единица измерения плотности. Атмосферное давление равно 1.013 X 105 Н/м2 (в единицах СИ), или, что то же самое, 14.7 фунта/кв. дюйм (в британской системе единиц). Однако, поскольку атмосфера и вода находятся в состоянии равновесия, элемент рА в нашей формуле можно отбросить.
Вода и волны 351 Пока мы занимались определением выталкивающей силы, действующей на материальную точку. Для игр решение такой задачи приносит мало пользы, но оно дает нам основу для вычисления выталкивающей силы, действующей на трехмерное тело. Если мы будем рассматривать твердое тело диаметром, скажем, в 1 метр, то давление на него снизу будет немного больше давления сверху, как показано на рисунке 13.3. В результате сила, толкающая его вверх вследствие плавучести, будет равна весу воды, которая может поместиться в объеме тела. Вес этой вытесненной воды пропорционален плотности воды, умноженной на ускорение силы тяжести. Другими словами, окончательная формула для вычисления выталкивающей силы будет выглядеть так: Fb = pgV В этой формуле V есть объем вытесненной воды. При написании игры вам придется вычислять объем помещенного в воду объекта. Для большинства игр достаточно делать это приближенно. Точный объем корпуса корабля вычислить весьма сложно. Вместо этого можно вычислить его приближенно, заменив корпус набором простых фигур. Например, можно считать его параллелепипедом, а острый нос корабля считать пирамидой или клином. Результат, полученный при таких заменах, будет приближенным, но достаточно точным для подавляющего большинства игр. Сопротивление движению Из повседневной жизни мы знаем, что двигаться в воде труднее, чем в воздухе. Причина этого - в вязкости воды. Вязкость - это мера «густоты» жидкости. Вязкость некоторых жидкостей, например, большинства машинных масел, больше вязкости воды. Замедление движения объектов в вязких жидкостях на самом деле связано с трением. Когда объект движется в жидкости, жидкость движется вокруг объекта. Трение между объектом и жидкостью приводит к рассеянию части энергии движения. Поэтому объект будет замедляться. В главе 12 мы разбирались с системами масс и пружин. Чтобы сделать их более стабильными, мы использовали дополнительную гасящую силу, которую назвали линейным затуханием. На самом деле эта сила была трением. Краткий обзор трения Чтобы разобраться с трением в жидкостях, давайте сначала посмотрим, как оно проявляет себя вообще. Представьте себе машину, едущую по улице. Иногда она разгоняется, иногда (когда водитель замечает полицейских) замедляет свое движение. Все это возможно благодаря трению. На рисунке 13.7 показано, как трение работает в данном случае.
352 Глава 13 На этом рисунке показано колесо движущегося автомобиля. Точка Р есть точка соприкосновения колеса и дороги. При вращении колеса оно отталкивается от дороги с силой Ft. Благодаря наличию силы трения дорога противодействует этой силе с силой Гг. Если дорога обледенела, то трение между дорогой и шиной будет практически нулевым, и сила Fr тоже будет маленькой. Колеса будут вращаться, но машина не сдвинется с места. Рис. 13.7. Силы, действующие на катящееся колесо Если объект катится по поверхности, как колеса автомобиля, то говорят, что между этим объектом и поверхностью есть трение качения (static friction). Трение качения возникает, если точка контакта между объектом и поверхностью не скользит по этой поверхности. Поэтому, если точка Р на рисунке 13.7 катится, а не скользит по земле, то говорят, что между землей и шиной существует трение качения. Вот формула для трения качения: Fs < nsN В этой формуле jus есть коэффициент трения качения, а N - нормальная сила. Нормальная сила для катящихся по горизонтальной поверхности объектов обычно выражается формулой N = -mg Коэффициент трения качения есть постоянная величина, определяемая свойствами рассматриваемых объектов. Например, коэффициент трения качения между шиной и асфальтом будет отличаться от коэффициента трения качения между шиной и льдом. Если водитель резко нажимает на тормоза, колеса блокируются и начинают скользить по дороге. При этом трение качения заменяется трением скольжения. Трение скольжения (dynamic friction) появляется, если объект скользит по поверхности.
Вода и волны 353 Формула для него выглядит так: D ;"DN Как видите, в формуле участвует коэффициент трения скольжения, определяющий величину трения между соприкасающимися объектами. Этот коэффициент обычно меньше коэффициента трения качения. Именно поэтому на курсах вождения инструктор предупреждает вас, что нельзя резко нажимать на тормоза - торможение будет более эффективным, если вы плавно надавите на тормоз. Если колеса вращаются, то действует коэффициент трения качения, и торможение выполняется эффективнее, чем в случае, когда колеса останавливаются и машина скользит по дороге. Чтобы увидеть, как работают все эти силы, взгляните на рисунок 13.8. wm AN fd F Рис. 13.8. Силы, действующие ▼ 9 на движущийся брусок На рисунке 13.8 показаны силы, действующие на прямоугольный брусок, который мы толкаем по поверхности. Вначале (верхняя часть рисунка) блок неподвижен. Сила тяжести Fg, равная mg, тянет блок вниз. Сила противодействия опоры N, действующая в обратном направлении, равна -mg.
354 Глава 13 В средней части рисунка 13.8 мы прикладываем к бруску силу F. Этой силе противодействует сила трения покоя Fs, определяемая свойствами бруска и поверхности. Если сила Fs больше или равна F, то брусок не сдвинется с места. Чтобы брусок начал двигаться, нужно, чтобы приложенная к бруску сила была больше, чем сила трения покоя. В нижней части рисунка 13.8 показан брусок в движении. F больше, чем Fs, и блок начинает скользить. В результате трение покоя больше не действует, на смену ему пришло трение скольжения. Это основы трения. А теперь вернемся к рассмотрению объектов, движущихся в воде. Вязкость Трение объекта, движущегося сквозь вязкую жидкость - это разновидность трения скольжения. Почему это так, иллюстрирует рисунок 13.9. Рис. 13.9. Объект, движущийся сквозь жидкость Линии, огибающие сферу на рисунке 13.9, упрощенно показывают течение жидкости вокруг сферы при движении этой сферы. Как видите, сфера и жидкость скользят относительно друг друга. А если они скользят, значит, действует трение скольжения. Поскольку сфера на рисунке 13.9 двигается не слишком быстро, линии, изображающие течение воды, остаются плавными. Говоря другими словами, течение жидкости не становится турбулентным (вихреобраз- ным) вследствие движения сферы. Пока линии потока не станут турбулентными, сила вязкого трения жидкости выражается формулой FFD = -CFDv Здесь Fpj, — сила вязкого трения, действующая на объект. CFD - это коэффициент вязкости, аналогичный коэффициенту трения скольжения в формуле из предыдущего раздела, v - это скорость объекта. Знак минуса указывает, что сила трения действует в направлении, противоположном направлению движения объекта. Эту формулу можно использовать в большинстве игр для определения силы трения, действующей на движущиеся в воде объекты. Этими объектами могут быть корабли, подводные лодки и другие транспортные средства. Но учтите, что если объект двигается в воде с большой скоростью, то движение воды вокруг него становится турбулентным. В этом случае нужно использовать формулу: :Яик£
Вода и волны 355 FFD=-CFDv2 Если вследствие движения объекта в воде вокруг него появляются вихри, сопротивление движению резко возрастает. При этом сила трения становится пропорциональной не скорости движения объекта относительно воды, а квадрату этой скорости. Эта формула пригодна для моделирования движения высокоскоростных катеров или футуристических транспортных средств. Течения в воде Часто мы хотим, чтобы в наших играх вода двигалась. Движущаяся вода образует течения, волны, прибой и водовороты. Все эти явления основываются на одних и тех же принципах. Рассмотрим трубу, изображенную на рисунке 13.10. Диаметр этой трубы постоянен по всей ее длине. Поэтому для любого сечения трубы А, перпендикулярного ее оси, мы можем найти массу воды, которая проходит сквозь это сечение, по формуле: Amw = pAvAt Рис. 13.10. Вода, текущая сквозь трубу постоянного диаметра Эта формула говорит нам, что масса Дш жидкости, проходящая сквозь трубу за интервал времени At, определяется площадью сечения трубы А, плотностью жидкости р и скоростью ее движения v. При этом подразумевается, что в трубе нет источников воды или отверстий, сквозь которые будет уходить часть воды, то есть через правый конец трубы будет выходить то же количество воды, что попало в трубу через левый конец. Эту формулу можно использовать, даже если труба сужается или расширяется, как на рисунке 13.11. Ai P^=F\ \l \J Рис. 13.11. Вода, движущаяся через трубу изменяющегося диаметра
356 Глава 13 Еще раз заметим — мы подразумеваем, что в трубе нет ни источников, ни отверстий, кроме как на ее концах. Если это условие выполняется, то через сечение А-^ за интервал времени At должно проходить та же масса воды, что и через сечение А2 за тот же интервал времени. Если бы через А2 проходила меньшая масса воды, это бы значило, что вода накапливается в трубе. Но этого не может быть - вода несжимаема. Если через А2 будет протекать больше воды, чем через Aj, труба скоро опустеет, хотя вода и будет продолжать поступать. Это тоже невозможно. Поэтому мы можем записать такое равенство: yoAjVjAt = pA2v2At Плотность воды не изменяется при постоянной температуре, поэтому ее можно исключить из равенства. Интервалы времени в обеих частях равенства тоже одинаковые, поэтому можно исключить и их. Тогда у нас останется равенство вида: Alvl = A2V2 Следовательно, если А2 больше, чем Aj, то vx должно быть больше, чем v2. Другими словами, если труба расширяется, то скорость течения воды в ней падает. Мы и так знали это из повседневной жизни, но теперь у нас есть математическое доказательство. Как применить эти формулы в играх? Предположим, что мы пишем игру, в которой герой пересекает реку. Эта река запружена выше места его переправы. Представим себе, что дамба, запруживающая реку, заминирована. Переправившись через реку, герой прячется в засаде и ждет, когда в реку войдут плохие парни. Как только плохие парни оказываются в реке, герой подрывает дамбу, и к плохим парням несется стена воды. И что же произойдет? Погибнут ли плохие парни, или их просто унесет, и герою придется с ними встретиться еще раз? Было ли в водохранилище перед дамбой достаточно воды, чтобы угробить плохих парней? Чтобы ответить на эти вопросы, нужно найти силу, с которой вода ударит плохих парней. Как это сделать? Вы, вероятно, догадались, что мы опять используем формулу F = та. Подставив эту формулу в первую формулу этого раздела, мы получим: Fw = pAvAtaw Сила, которую вода приложит к плохим парням, равна массе воды (pAvAt), умноженной на ускорение. Чье ускорение? Ускорение плохих парней. Предположим, что их скорость возрастает с нуля до скорости движения воды за интервал времени At. Тогда формула будет выглядеть так: Fw=pAvwAt
Вода и волны 357 Поскольку конечная скорость плохих парней будет равна скорости воды, то vBG равна скорости воды vw. Кроме того, At в числителе и знаменателе равны друг другу, и их можно сократить. Тогда финальный вариант формулы будет таким: Fw = PAv2 В качестве площади А нужно использовать площадь поперечного сечения реки в месте дамбы. Если эта площадь велика (значит, воды много), и если вода двигалась быстро, то плохим парням конец. А если нет, значит, герою еще придется повстречаться с ними. Волны Еще один способ движения воды - движение, связанное с распространением волн. Волны, распространяющиеся по поверхности воды, называются поперечными волнами (transverse waves). В поперечных волнах колеблющаяся среда перемещается в направлении, перпендикулярном направлению распространения волн. Они показаны на рисунке 13.12. Рис. 13.12. Поперечные волны на поверхности воды Волна на рисунке 13.12 движется слева направо, а вода движется вверх или вниз, то есть в направлении, перпендикулярном направлению распространения волн. В действительности волны в воде не являются строго поперечными. Если поместить мячик для пинг-понга (или другой легкий объект) в волнующуюся воду, то мячик будет постепенно смещаться в направлении распространения волн. Следовательно, и сама вода тоже смещается в этом направлении. Однако в основном вода перемещается в вертикальном направлении. Волны, распространяющиеся по поверхности воды от центра, образуют круги. Это можно увидеть, бросив в спокойную воду любой предмет. Но волны только кажутся нам кругами — мы не видим того, что происходит в глубине воды. На самом деле волны распространяются в виде сфер.
358 Глава 13 На рисунке 13.13 показана ударная волна, возникающая при подводном взрыве. Волна распространяется в виде сферы во всех направлениях от места взрыва. Между прочим, сила этой волны не может быть больше, чем она была в момент взрыва. Можно также сказать, что общая мощность волны не изменяется во времени (мощность и сила — это немного разные вещи). Мощность волны распределена равномерно по всей сферической поверхности фронта этой волны. По мере того, как волна уходит все дальше от места взрыва, поверхность ее сферы все увеличивается, и интенсивность волны становится все меньше. Интенсивность волны в любой точке можно найти по формуле: 4кг2 Фронт распространения ,. волны Точка взрыва / ч \ х \ L БУХ ! J n. ./ Рис. 13.13. Сферическая подводная ^-—■*— волна Эта формула часто оказывается очень полезной в 3D-nrpax, когда нужно моделировать ударные волны от взрывов. Вначале ударной волне назначаются некоторая сила, мощность и интенсивность. Давайте обозначим их Fj, Рг и 1-у. По прошествии времени t они изменятся до F2, Р2 и 12. В играх мы моделируем физические явления, моделируя силы. Больше всего нам нужна величина F2. Представим, к примеру, что вы пишете игру, в которой происходит подводный взрыв. Ваш герой уходит от места взрыва на подводной лодке так быстро, как только можно. Когда взрыв происходит, вам нужно определить, какие повреждения получит подводная лодка от ударной волны. Проще всего получить ответ на этот вопрос, моделируя ударную волну как объект с двумя свойствами: скоростью распространения и начальной точкой, из которой эта волна распространяется. Одна из сил, называемая продольной силой (longitudinal force), направлена прямо от начальной точки (места взрыва). Другая сила, называемая поперечной силой (transverse force), направлена перпендикулярно продольной. Поперечная сила будет уменьшаться с увеличением расстояния до точки взрыва. Скорость, которую мы будем хранить в объекте ударной волны - это скорость перемещения ее фронта. В реальных взрывах эта скорость зависит от характеристик взрыва и свойств среды, в которой распространяется ударная волна. Однако в играх можно просто задать эту скорость и не изменять ее, пока ударная волна не рассеется.
Вода и волны 359 Чтобы найти величины сил, которые ударная волна прикладывает к объектам, удаляясь от своего источника, можно воспользоваться взаимоотношениями ее свойств. Как вы помните, мы обозначили начальные силу, мощность и интенсивность как Fj, P± и Ij. Сила, мощность и интенсивность по прошествии времени t будут равны F2, P2 и 12. Мы знаем, что справедливы такие соотношения: F2 <Fj P2=Pl Можно также утверждать, что соотношение Fj и F2 будет равно соотношению Ij и 12. В виде формулы это соотношение будет таким: *1 Ii Из этого соотношения можно найти F2: h Если в это выражение подставить формулу интенсивности, то мы получим такой результат: ?2 F,= F,—-2- 2 1 р 4л г2 Немного неудобно, правда? В этом выражении гх - это начальный радиус ударной волны, а г2 - радиус ударной волны, для которого нам нужно найти силу. Но для начала давайте упростим наше выражение. Рг равно Р2, поэтому их можно убрать из формулы. 4тг есть и в числителе, и в знаменателе, поэтому их тоже можно убрать. У нас осталась гораздо более удобная формула г2 Р2=РЛ h Этой формулой воспользоваться несложно - нам известны Fj и г1 (вы задаете их, когда пишете игру). Вы просто решаете, какие начальный радиус и начальную силу задать взрыву. Кроме того, вы задаете место, в
360 Глава 13 котором происходит взрыв, и скорость распространения ударной волны. По прошествии времени t ваша программа использует данные о скорости волны, чтобы найти радиус ударного фронта. Затем по только что полученной нами формуле можно найти силы, которые прикладывает ударная волна в выбранной точке. Эту формулу можно применять как к продольным, так и к поперечным силам, то есть: 2 h И г2 F =F -L r2L rlL 2 h Здесь F1T и F1L - начальные поперечная и продольная силы, a F2t и F2L ~~ эти же силы по прошествии времени t. Вода в программах В разное время для моделирования воды в играх применялись самые разные способы. Мы рассмотрим наиболее распространенные способы - от самых простых до самых сложных. Простые способы имитации В большинстве игр вода на самом деле вообще не моделируется. Вам кажется, что в игровом мире присутствует вода, но это только иллюзия, на создание которой уходит очень немного процессорного времени. Есть несколько способов такой имитации. Простейший способ - просто игнорировать тот факт, что вода - это вода. И часто такой подход дает вполне сносные результаты. Например, вы наверное видели программу-скринсейвер, в которой по экрану плавают рыбки. Если присмотреться, то фон голубого цвета и несколько неподвижных объектов создают впечатление, что на экране изображен коралловый риф. При этом подразумевается, что вы знаете - коралловые рифы и рыбы находятся под водой. В этой программе не моделируются и не отображаются ни волны, ни рябь, ни пузырьки. Программист просто считает, что вы и так поймете, что изображен подводный мир. Такой подход использовался во многих играх. Хотя он и работает, но дает не такие реалистичные результаты, как более сложные методы. В играх, в которых герой никогда не попадает в воду, вода часто изображается просто поверхностями с нанесенными текстурами, как на рисунке 13.14.
Вода и волны 361 Ш.,- + Рис. 13.14. Бассейн с водой, реализованный в виде текстурированного полигона В основе этого метода лежит полигон, форма которого соответствует форме бассейна с водой. Затем вы наносите на этот полигон текстуру поверхности воды. Эта текстура не обязательно должна быть той же формы, что и полигон. Собственно говоря, обычно эти текстуры квадратные или прямоугольные, как на рисунке 13.14. Нанеся текстуру на полигон, вы получите симпатичный бассейн с водой. На рисунке к бассейну добавлено окаймление из камней, чтобы он выглядел более реалистично. Чтобы добавить реализма, можно наносить на полигон новую текстуру в каждом кадре, чтобы вода казалась движущейся. Что можно делать с бассейном, изображенным таким образом? Да почти все, что угодно. Этот бассейн может быть просто частью интерьера, а может быть священным колодцем, в котором живет оракул, дающий герою советы и исцеляющий его раны. Трехмерная вода Цель этой главы - разобраться, как использовать физические законы для моделирования объектов, находящихся в воде. Так как же моделировать воду в 3D и позволить объектам и персонажам двигаться в ней? Есть несколько методов такого моделирования. Часто используется модифицированная версия метода, показанного на рисунке 13.14. Мы просто наносим текстуру не на полигон, а на верхнюю поверхность трехмерного объекта. Рис. 13.15. Трехмерное представление бассейна с водой
362 Глава 13 На рисунке 13.15 показан бассейн с водой из рисунка 13.14, но текстура поверхности воды нанесена не на плоский полигон, а на верхний торец цилиндра. Теперь персонаж может прыгнуть в бассейн и нырнуть в него, чтобы достать со дна священные реликвии. Пока герой находится в воде, его поведение будет моделироваться согласно формулам для плавучести, силы тяжести, вязкости воды и так далее. Эти формулы мы получили ранее в этой главе. Если мы рассматриваем большой водоем - озеро или море - то наша задача становится более сложной. В качестве внешнего представления воды все равно используется трехмерный объект. Замечание Как вы, вероятно, помните из главы 12, мы можем создавать внутренние представления объектов, которые используются в программах для выполнения расчетов. Пользователь видит внешнее представление объекта - обычно это сетчатая модель. Чтобы создать нечто похожее на озеро или море, нужно создать сетчатую модель ЗБ-объекта в программе 3D-моделирования, например, Milk- Shape3D, которая есть на компакт-диске, прилагающемся к книге. Эта программа находится в папке Tools\MilkShape3D. Замечание Копия MilkShape3D, находящаяся на компакт-диске - это 30-дневная пробная версия программы. Чтобы купить зарегистрированную версию, обратитесь в chUmbaLum sOft по адресу http://www.swissquake.ch/ chumbalum-soft/. Создав сетчатую модель для озера, нужно создать кусок ткани, который будет внутренним представлением поверхности воды. Почему именно кусок ткани? Потому что модель ткани в том виде, в каком мы ее создали в главе 12, можно использовать для представления поверхности воды. Волны будут распространяться по куску ткани точно так же, как и по поверхности воды. Если немного повозиться, подбирая параметры ткани, то можно получить очень правдоподобные медленно перекатывающиеся волны. Сделав это, нужно совместить в программе позиции частиц ткани и позиции вертексов сетчатой модели, изображающей поверхность воды. Затем к находящимся в воде объектам нужно применить уравнения, полученные ранее в этой главе. Результат будет очень реалистичным и впечатляющим. У такого подхода есть только один недостаток. Это очень дорогостоящий метод реализации воды с точки зрения вычислительной нагрузки на процессор. Однако производительность процессоров непрерывно увеличивается, и для вас этот недостаток может оказаться не слишком существенным.
Вода и волны 363 Объекты в воде Мы разобрались с теоретическими основами. Давайте попробуем применить их на практике. Начнем со встраивания уравнений плавучести и вязкого трения в класс твердых тел. Затем мы создадим программу, которая продемонстрирует эти уравнения в работе. Добавление плавучести в класс твердых тел Добавить плавучесть в платформу физического моделирования несложно. Пожалуй, проще всего использовать для этого механизм наследования C++. В этой главе я создал класс immersible__rigid_body. Этот класс порожден от класса rigid_body. Если объект класса immersib- le_rigid_body не погружен в воду, он ведет себя точно так же, как и объект класса rigid_body. Код класса immersible_rigid_body приведен в листинге 13.1. Листинг 13.1. Класс immersible_rigid_body 1 class immersible_rigid_body : public rigid_body 2 < 3 private: 4 scalar volume; 5 bool isInWater; 6 bool buoyancyApplied; 7 scalar dragCoefficient; 8 bool constantForceChanged; 9 bool irapulseForceChanged; 10 11 public: 12 immersible_rigid_body(); 13 14 void Volume( 15 scalar shapeVolume); 16 scalar Volume(void); 17 18 void BuoyancyForce(force buoyancy); 19 force BuoyancyForce(void); 20 21 void IsImmersed(bool inWater); 22 bool Islmmersed(void); 23 24 void Mass( 25 scalar massValue); 26 scalar Mass(void); 27
364 Глава 13 28 void ConstantForce( 29 force sumConstantForces); 30 force ConstantForce(void); 31 32 void ImpulseForce( 33 force sumlmpulseForces); 34 force ImpulseForce(void); 35 36 void DragCoefficient( 37 scalar resistance); 38 scalar DragCoefficient(void); 39 40 bool Update( 41 scalar changelnTime); 42 }; Класс rigid_body открыто унаследован в классе immersible_ri- gid_body. Его private-элементы данных предназначены для хранения информации, которая не хранилась в объектах класса rigid_body. Во-первых, для объекта класса immersible_rigid_body нужно хранить его объем. Объем нужен для вычисления плавучести, то есть веса воды, которую вытесняет объект. Значение элемента данных volume, в котором хранится объем, всегда должно быть больше 0. Замечание Значение элемента данных volume объекта класса immersible_ri- gid_body должно быть равно объему части объекта, погруженной в воду. Если объект полностью погружен в воду, то это значение должно быть равно полному объему объекта. Если объект плавает на поверхности воды, значение должно быть равно объему части объекта, находящейся ниже уровня воды. Кроме того, в классе immersible_rigid_body есть переменная состояния, объявленная в строке 5 листинга 13.1. Эта переменная отслеживает, находится ли объект в воде. Каждый раз, когда объект попадает в воду, программа должна вызывать метод, который установит эту переменную в значение true. Когда объект покидает воду, значение этой переменной должно быть сброшено в false. Когда объект попадает в воду, класс immersible_rigid_body должен приложить к нему выталкивающую силу, определяющуюся плавучестью объекта. Эта сила будет действовать постоянно, пока объект будет оставаться в воде. Поэтому в объектах класса immersible_rigid_body выталкивающая сила хранится как постоянно действующая сила. После того, как величина этой силы сохранена в элементе ConstantForce, она не должна прикладываться вновь. В классе immersible_rigid_body есть элемент данных buoyancyApplied, который следит за этим. Класс автоматически инициализирует этот элемент значением false при создании
Вода и волны 365 объекта. Когда класс iimersible__rigid_body прикладывает выталкивающую силу к объекту, он устанавливает в true переменную buoyancyAp- plied. После этого значение переменной buoyancyApplied не должно изменяться до тех пор, пока объект не будет извлечен из воды, либо не изменится его масса или объем. Если это происходит, то переменная buoyancyApplied сбрасывается в false. В элементе данных dragCoef ficient, объявленном в строке 7 листинга 13.1, хранится коэффициент вязкого трения. Его значение должно лежать в пределах от 0 до 1. Чем больше это значение, тем сильнее жидкость увлекает за собой находящиеся в ней объекты. Большие значения этого элемента данных соответствуют более вязким жидкостям. В примере программы из этой главы для этого коэффициента выбрано значение 0.5. Оно вполне подходит для большинства игр, но, возможно, вы захотите подобрать более подходящее для вашей игры значение. Замечание Значение коэффициента вязкого трения зависит от материала объекта, от формы этого объекта и свойств жидкости, сквозь которую он движется. Даже если все объекты в вашей игре движутся только в воде, нельзя подобрать единственное значение, которое подойдет для всех объектов. Потратив некоторое время на подбор разных значений этого коэффициента для разных объектов, вы получите гораздо более реалистичное движение. Трение действует на объект все время, пока этот объект движется. Однако оно должно влиять на все приложенные к объекту силы. Оно уменьшает воздействие и постоянно действующих, и импульсных сил. Класс immersible_rigid_body учитывает воздействие трения на постоянно действующие и импульсные силы при каждом их изменении. Чтобы отследить изменения сил, в классе immersible_rigid_body определены элементы данных constantForceChanged и impulseForceChanged. Если какой-то из этих элементов содержит значение true, то класс immer- sible_rigid_body учтет воздействие трения на соответствующие силы. В классе immersible_rigid_body содержатся public-методы для чтения и установки значений элементов данных. Однако заметьте, что в этом классе переопределены перегруженные методы Mass (), Constant- Force () и ImpulseForce (). Кроме того, переопределен метод Update (). Чтобы понять, зачем это сделано, взгляните на листинг 13.2. Листинг 13.2. Методы класса immersible_rigid_body 1 inline immersible_rigid_body::immersible_rigid_body() : 2 rigid_body() 3 { 4 volume = 0.0; 5 IsInWater = false; 6 buoyancyApplied = false;
366 Глава 13 7 dragCoefficient = 0.0; 8 constantForceChanged = false; 9 impulseForceChanged = false; 10 } 11 12 inline void immersible_rigid_body::Volume( 13 scalar shapeVolume) 14 { 15 volume = shapeVolume; 16 buoyancyApplied = false; 17 } 18 19 inline scalar immersible_rigid_body::Volume(void) 20 { 21 return (volume); 22 } 23 24 inline void immersible_rigid_body::lslmmersed( 25 bool inWater) 26 { 27 isInWater = inWater; 28 } 29 30 inline bool immersible_rigid_body::IsImmersed(void) 31 { 32 return (isInWater); 33) 34 35 inline void immersible_rigid_body::Mass( 36 scalar massValue) 37 { 38 rigid_body::Mass(massValue); 39 buoyancyApplied = false; 40 } 41 42 inline scalar immersible_rigid_body::Mass(void) 43 { 44 return (rigid_body::Mass()); 45 ) 46 47 inline void immersible_rigid_body::ConstantForce( 48 force sumConstantForces) 49 { 50 rigid_body::ConstantForce(sumConstantForces); 51 constantForceChanged = true; 52 } 53 54 inline force immersible_rigid_body::ConstantForce(void) 55 {
Вода и волны 367 56 return (rigid_body::ConstantForce()); 57 } 58 59 inline void iramersible_rigid_body::ImpulseForce{ 60 force sumlmpulseForces) 61 { 62 rigid_body::ImpulseForce(sumlmpulseForces); 63 impulseForceChanged = true; 64 } 65 66 inline force immersible_rigid_body::ImpulseForce(void) 67 { 68 return (rigid_body::ImpulseForce()); 69 } 70 71 inline void immersible_rigid_body::DragCoefficient( 72 scalar resistance) 73 { 74 assert((dragCoefficient=0.0) && (dragCoefficient<=l.0) 75 dragCoefficient = resistance; 76 } 77 78 inline scalar immersible_rigid_body::DragCoefficient(void) 79 { 80 return (dragCoefficient); 81 ) 82 bool immersible_rigid_body::Update( 83 scalar changelnTime) 84 { 85 // Если объект находится в воде... 86 if (isInWater) 87 { 88 vector_3d tempVector; 89 force tempForce; 90 91 // Если плавучесть еще не учтена (или изменилась). 92 if (!buoyancyApplied) 93 { 94 // Вычисляем выталкивающую силу. 95 tempVector.SetXYZ@.0,1.Of,0.0); 96 tempForce.Force( 97 9.8f*volume*tempVector); 98 tempVector = Location(); 99 tempForce.ApplicationPoint(tempVector); 100 tempForce.Force( 101 tempForce.Force() + 102 rigid_body::ConstantForce().Force()) ; 103 ConstantForce(tempForce); 104
368 Глава 13 105 buoyancyApplied=true; 106 } 107 108 // Если постоянно действующие силы изменились... 109 if (constantForceChanged) 110 { 111 // Учитываем сопротивление воды (вязкое трение). 112 tempForce.Force( 113 ConstantForce().Force() - 114 (dragCoefficient * ConstantForce().Force())); 115 ConstantForce(tempForce); 116 constantForceChanged = false; 117 } 118 119 // Если импульсные силы изменились... 120 if (impulseForceChanged) 121 { 122 // Учитываем сопротивление воды (вязкое трение). 123 tempForce.Force( 124 ImpulseForce().Force() - 125 (dragCoefficient * ImpulseForce().Force())); 126 ImpulseForce(tempForce); 127 impulseForceChanged = false; 128 } 129 } 130 131 /* Обновляем данные о положении и ориентации погруженного 132 тела, исходя из действия приложенных к нему сил. */ 133 return (гigid_body::Update(changeInTime)); 134} Предупреждение В листинге 13.2 приведен код встраиваемых методов из файла PMlmmer- sibleRigidBody. h и код метода Update () из файла PMImmersibleRi- gidBody.cpp. В книге они объединены в один листинг для удобства, но в программе эти методы находятся в разных файлах. Большинство методов из листинга 13.2 предназначены для чтения и записи значений соответствующих элементов данных в объектах класса immersible_rigid_body. Но обратите внимание на ряд дополнительных действий, которые выполняются в этих методах. Например, в методе Volume () (строки 12-17) не только задается новое значение объема объекта. При каждом изменении объема нужно пересчитывать плавучесть объекта, поэтому метод сбрасывает переменную buoyancyApplied в значение false. Кроме того, плавучесть необходимо вычислять заново, если объект класса immersible_rigid_body попадает в воду или покидает ее.
Вода и волны 369 Метод Mass () переопределен в классе immersible_rigid_body по той же причине. Если масса объекта изменится, значит, изменится сила тяжести, действующая на него, и он может утонуть или всплыть. В любом случае величину силы тяжести нужно вычислять заново, как и плавучесть. Поэтому в строке 39 листинга 13.2 метод сбрасывает переменную buoyancyApplied в значение false. Если изменяются постоянно действующие силы, то трение тоже нужно пересчитывать. Поэтому в классе immersible_rigid_body переопределен метод ConstantForce () класса rigid_body. Версия этого метода в классе immersible_rigid_body не только задает новую величину постоянно действующей силы, но и устанавливает элемент данных cons- tantForceChanged в значение true. Это приведет к тому, что метод immersible_rigid_body: : Update () пересчитает воздействие вязкого трения на объект. Как уже говорилось, трение влияет как на постоянно действующие, так и на импульсные силы. Поэтому в классе immersible_rigid_body переопределены и методы ImpulseForce (). Метод Update (), начинающийся со строки 82, проверяет, находится ли объект в воде. Если да, то метод проверяет, учтено ли воздействие плавучести на объект. Если это еще не сделано, то метод Update () создает единичный вектор, направленный вертикально вверх. Затем он вычисляет величину вектора плавучести (строки 96-99). Обратите внимание, что в вычислениях не фигурирует плотность воды - она считается равной единице, и учитывать ее в качестве множителя незачем (в формуле выталкивающей силы F = pgV значение р будет равно 1). Метод Update () добавляет выталкивающую силу к уже учтенным постоянно действующим силам в строках 100-102. Результат сложения сил сохраняется как величина постоянно действующей на объект силы (строка 103). Затем метод устанавливает переменную buoyancyApplied в значение true. В строках 108-127 листинга 13.2 учитывается воздействие трения на силы. Если воздействие трения на постоянно действующие и импульсные силы не учитывалось, то метод Update () учитывает его и вычисляет новые значения сил. Пример программы В примере программы из этой главы демонстрируются плавучесть, сопротивление воды (вязкое трение) и простые течения. В листинге 13.3 показана инициализация объектов для этой программы. Эта программа моделирует три помещенных в воду шарика. Первый слева шарик всплывает, второй — тонет, а третий находится в воде в состоянии равновесия. Он не тонет и не всплывает. На этот шарик действует слабое течение, которое уносит его вправо.
370 Глава 13 Листинг 13.3. Инициализация примера программы 1 bool Gamelnitialization() 2 { 3 // Создаем матрицу отображения - как в предыдущих примерах. 4 D3DXVECTOR3 eyePoint@.Of,0.Of,-20.Of); 5 D3DXVECTOR3 lookatPoint@.Of,0.Of,0.Of) ; 6 D3DXVECTOR3 upDirection@.Of,1.Of,0.Of); 7 D3DXMATRIXA16 tempViewMatrix; 8 D3DXMatrixLookAtLH( 9 StempViewMatrix,SeyePoint,&lookatPoint,SupDirection); 10 theApp.ViewMatrix(tempViewMatrix); 11 12 // Создаем матрицу проецирования. 13 D3DXMATRIXA16 projectionMatrix; 14 D3DXMatrixPerspectiveFovLH( 15 SprojectionMatrix,D3DX_Pl/4,1.0f,1.0f,100.Of); 16 theApp.ProjectionMatrix(projectionMatrix); 17 18 // 19 // Задаем свойства первого шарика. 20 // 21 vector_3d zeroVector@.0,0.0,0.0); 22 allBalls[0].LinearVelocity(zeroVector); 23 allBalls[0].LinearAcceleration(zeroVector); 24 allBalls[0].BoundingSphereRadius(BOUNDING_SPHERE_RADIUS); 25 allBalls[0].CoefficientOfRestitution(COFFICIENT_OF_RESTITUTION); 26 allBalls[0].AngularAcceleration(zeroVector); 27 allBalls[0].AngularVelocity(zeroVector); 28 allBalls[0].CurrentOrientation( 29 angle_set_3d@.0,0.0,0.0)) ; 30 force tempForce; 31 tempForce.Force(zeroVector) ; 32 allBalls[0].ImpulseForce(tempForce); 33 allBalls[0].Islmmersed(true); 34 // Объем сферы равен 4/3*pi*rA3. 35 allBalls[0].Volume( 36 4.Of/3.Of * D3DX_PI * 37 BOUNDING_SPHERE_RADIUS * 38 BOUNDING_SPHERE_RADIUS * 39 BOUNDING_SPHERE_RADIUS); 40 allBalls[0].LoadMesh("ball2.x"); 41 allBalls[0].DragCoefficient(COEFFICIENT_OF_WATER_DRAG); 42 43 // Копируем данные для других шариков. 44 allBalls[2]=allBalls[l]=allBalls[0]; 45 46 // Задаем значения остальных свойств первого шарика. 47 allBalls[0].Mass(BALL1_MASS); 48 allBalls[0].Location(BALL1 LOCATION);
Вода и волны 371 49 scalar inertiaValue = 50 B * BALL1_MASS * 51 BOUNDING_SPHERE_RADIUS * BOUNDING_SPHERE_RADIUS)/5; 52 allBalls[0].Rotationallnertia( 53 vector_3d(inertiaValue,inertiaValue,inertiaValue)); 54 55 // Прикладываем силу тяжести к первому шарику. 56 tempForce.Force( 57 vector_3d{ 58 O.Of, 5 9 GRAVITATIONAL_ACCELERATION*BALLl_MASS, 60 O.Of)); 61 tempForce.ApplicationPoint(BALLl_LOCATION); 62 allBalls[0].ConstantForce(tempForce); 63 64 // Задаем значения остальных свойств второго шарика. 65 allBalls[1].Mass(BALL2_MASS); 66 allBalls[1].Location(BALL2_LOCATION); 67 inertiaValue = 68 B * BALL2_MASS * 69 BOUNDING_SPHERE_RADIUS * BOUNDING_SPHERE_RADIUS)/5; 70 allBalls[1].Rotationallnertia( 71 vector_3d(inertiaValue,inertiaValue,inertiaValue)); 72 73 // Прикладываем силу тяжести ко второму шарику. 74 tempForce.Force( 75 vector_3d( 76 O.Of, 77 GRAVITATIONAL_ACCELERATION*BALL2_MASS, 78 O.Of)); 79 tempForce.ApplicationPoint(BALL2_LOCATION); 80 allBalls[1].ConstantForce(tempForce); 81 82 // Задаем значения остальных свойств третьего шарика. 83 allBalls[2].Mass(BALL3_MASS); 84 allBalls[2].Location(BALL3_LOCATION); 85 inertiaValue = 86 B * BALL3_MASS * 87 BOUNDING_SPHERE_RADIUS * BOUNDING_SPHERE_RADIUS)/5; 88 allBalls[2].Rotationallnertia( 89 vector_3d(inertiaValue,inertiaValue,inertiaValue)); 90 91 // Прикладываем силу тяжести к третьему шарику. 92 tempForce.Force( 93 vector_3d( 94 O.Of, 95 GRAVITATIONAL_ACCELERATION*BALL3_MASS, 96 O.Of)); 97 tempForce.ApplicationPoint(BALL3_LOCATION); 98 allBalls[2].ConstantForce(tempForce); 99
372 Глава 13 100 // Прикладываем силу течения к третьему шарику. 101 tempForсе.Force( 102 vector_3d(l.Of,0.0,0.0) + 103 allBalls[2].ConstantForce().Force()); 104 allBalls[2].ConstantForce(tempForce); 105 106 // 107 // Задаем рассеянный направленный свет. 108 // 109 D3DLIGHT9 light; 110 ZeroMemory( Slight, sizeof(light) ); 111 light.Type = D3DLIGHT_DIRECTIONAL; 112 113 D3DXVECTOR3 vecDir; 114 vecDir = D3DXVECTOR3@.Of, -l.Of, l.Of); 115 D3DXVec3Normalize((D3DXVECTOR3*)Slight.Direction,SvecDir); 116 117 // Задаем цвет рассеянного направленного света 118 light.Diffuse.r = l.Of; 119 light.Diffuse.g = l.Of; 120 light.Diffuse.b = l.Of; 121 light.Diffuse.a = l.Of; 122 theApp.D3DRenderingDevice()->SetLight@,Slight ); 123 theApp.D3DRenderingDevice()->LightEnable( 0, TRUE ); 124 theApp.D3DRenderingDevice()->SetRenderState( 125 D3DRS_DIFFUSEMATERIALSOURCE, 126 D3DMCS_MATERIAL); 127 128 return (true); 129 ) Как и в предыдущих примерах программ, функция Gamelnitiali- zation () начинается с создания матриц отображения и проецирования. Затем она инициализирует свойства первого шарика в строках 21-41. Это свойства, значения которых одинаковы для всех трех шариков. Например, у всех этих шариков одинаковые радиусы и коэффициенты трения. Программа копирует значения этих свойств в другие шарики в строке 44. Далее функция GameInitialization() задает свойства, уникальные для каждого шарика. Например, у каждого шарика своя масса, свое положение и своя величина вращательной инерции. Предупреждение Никогда не задавайте вращательную инерцию твердого тела равной нулю. Это физически невозможно. При написании этой программы я сам машинально сделал это, хотя и знал, что это невозможно. Поэтому я добавил некоторые проверки в метод rigid_body: :RotationalInertia() в файле PMRigidBody.h, чтобы сделать такую ошибку невозможной.
Вода и волны 373 После инициализации всех свойств шариков программа задает характеристики света (в строках 109-126). Теперь можно начать моделировать и отрисовывать шарики. В листинге 13.4 приведен код функций UpdateFrame () и RenderFrame (), которые этим занимаются. Листинг 13.4. Обновление и отрисовка кадра 1 bool UpdateFrame() 2 { 3 // Пора обновлять кадр? 4 DWORD currentTime = ::timeGetTime(); 5 if (!TimeToUpdateFrame(currentTime)) 6 return (true); 7 8 // Обновляем положение каждого шарика. 9 for (int i=0; i<TOTAL_BALLS; i++) 10 { 11 allBalls[i].Update(STEP_SIZE); 12 } 13 return (true); 14 } 15 16 bool RenderFrame() 17 { 18 // Задаем матрицу отображения, если она изменилась. 19 theApp.D3DRenderingDevice()->SetTrans form( 20 D3DTS_VIEW, 21 StheApp.ViewMatrix()); 22 23 // Задаем матрицу проецирования, если она изменилась. 24 theApp.D3DRenderingDevice()->SetTransform( 25 D3DTS_PROJECTION, 26 StheApp.ProjectionMatrix()); 27 28 // Отрисовываем каждый шарик. 29 for (int i=0; i < TOTAL_BALLS; i++) 30 { 31 allBalls[i].Render() ; 32 ) 33 34 return (true); 35 } Код функций UpdateFrame () и RenderFrame () весьма прямолинеен. Функция UpdateFrame () проверяет, пора ли обновлять кадр. Если да, она начинает выполнять цикл в строке 9 листинга 13.4, в котором вызывается метод immersible_rigid_body: :Update (). Код этого метода
374 Глава 13 приведен в листинге 13.2. Как мы уже видели ранее, метод immersib- le_rigid_body: : Update {) вычисляет выталкивающую силу и сопротивление воды, действующие на объект класса immersible_rigid_body. Затем он вызывает метод rigid_body: : Update (), чтобы найти новое положение объекта класса immersible_rigid_body исходя из действующих на него сил, включая силу тяжести и силы, вызванные течением воды. Функция RenderFrame () из листинга 13.4 мало отличается от таких же функций в предыдущих примерах. Задав матрицы отображения и проецирования, она просто вызывает метод immersible_rigid_body: : Render () для каждого шарика. Эта функция отрисовывает каждый шарик в буфер, который затем выводится на экран платформой физического моделирования. Запустив эту программу, вы заметите, что я выбрал самый простой способ изображения воды. Все, что я сделал — выбрал темно-синий фон с легким зеленоватым оттенком. Часто этого достаточно, чтобы игрок понял, что действие происходит под водой. Подсказка Пожалуй, лучше всего различные методы моделирования воды рассмотрены в книге Mason McCuskey «Special Effects Game Programming with DirectX» (издательство Premier Press). Эту книгу, думаю, стоит прочитать каждому, кто хочет создавать игры. Итоги Хотя моделирование воды - не такая уж простая задача, моделировать поведение погруженных в воду объектов довольно просто. Зная основные принципы плавучести, давления, вязкого трения, течений и волн, можно моделировать поведение большинства объектов в воде. Формулы, описывающие соответствующие соотношения, просты и широко применяются.
Часть III Практические примеры Глава 14 Готовимся создавать игры 376 Глава 15 Автомобили, корабли и лодки 415 Глава 16 Авиация и космические корабли 440 Эпилог 470
Глава 14 Готовимся создавать игры Как и в главе 6 «Сетчатые модели и Х-файлы», в этой главе не будет ни физики, ни математики. На данном этапе нам нужно ненадолго отойти от обсуждения физического моделирования, чтобы разобраться с вопросами, связанными с написанием игр. Точнее говоря, прежде чем двигаться дальше, нужно решить две важные задачи. Первая задача - усовершенствование платформы физического моделирования. Мы хотим, чтобы ее можно было использовать для полномасштабных игр, а не только для простых программ моделирования, которые мы создавали до сих пор. Чтобы эта платформа стала пригодной для игр, нужно улучшить ее структуру и добавить некоторые возможности, требуемые для работы с DirectX. Вторая задача - изучение некоторых дополнительных возможностей DirectX. Игры, в том числе и трехмерные, должны реагировать на действия пользователя. Предыдущие наши программы этого не делали, но прежде чем мы сможем двинуться дальше, нам нужно обсудить обработку действий пользователя с помощью Directlnput. Кроме того, в приведенных выше программах позиция наблюдения была фиксированной. В большинстве трехмерных игр позиция наблюдения перемещается в соответствии с передвижением главного персонажа. Переработка платформы физического моделирования Та платформа физического моделирования, которую мы использовали до сих пор, была удобным учебным инструментом. Она демонстрировала основы работы с DirectX и графикой, а так же воплощала физические законы, которые мы рассматривали. Но, чтобы использовать эту платформу для написания игр, нужно добавить в нее некоторые дополнительные возможности.
Готовимся создавать игры 377 Упрощение инициализации программы Первое, что нужно улучшить в платформе, - это метод ее инициализации. У Direct3D есть огромное количество переменных состояния, которые можно проинициализировать. Дабы не отвлекаться от вопросов моделирования физики, мы просто игнорировали большую часть этих переменных. Это не имело никаких неприятных последствий, поскольку Direct3D инициализирует их разумными значениями по умолчанию. Это удобно при написании программ, поскольку упрощает их. Накопив опыт работы с Direct3D и другими компонентами DirectX, вы, вероятно, захотите более тонко настраивать их при инициализации. Кроме того, вы, вероятно, пожелаете использовать собственные параметры инициализации окон. Инициализация платформы выполняется в файлах PMD3DApp.h и PMD3DApp.cpp. Предположим, что вы хотите сделать так, чтобы отсечение невидимых поверхностей можно было включать или отключать с помощью одного из параметров инициализации. Вот что нужно для этого сделать. 1. Добавить новый элемент данных в структуру d3d_init_j?a- rams. 2. Добавить новый элемент данных в класс d3d_app. 3. Добавить код для инициализации нового элемента данных в конструктор класса d3d_app. 4. Добавить в функцию d3d_app: :InitApp() код, копирующий значение элемента данных структуры d3d_init_j?arams в новый элемент данных класса d3d_app. 5. Добавить в функцию d3d_app: : InitD3D () код, использующий новый элемент данных класса d3d_app. Этот процесс можно заметно упростить. Сначала перепишем класс d3d_app так, чтобы он не содержал отдельных элементов для каждого параметра инициализации Direct3D и каждого параметра инициализации окна. Новая версия определения класса d3d_app приведена в листинге 14.1. Листинг 14.1. Версия класса d3d_app с упрощенной инициализацией 1 class d3d_app 2 { 3 private: 4 // Свойства приложения. 5 bool applnitialized; 6 7 // Свойства окна. 8 std::string windowTitle; 9 int defaultX, defaultY;
378 Глава 14 10 int defaultHeight,defaultWidth; 11 12 // Свойства D3D. 13 // Используется для создания D3DDevice 13 LPDIRECT3D9 direct3D; 14 LPDIRECT3DDEVICE9 d3dDevice; // Наше устройство рендеринга 15 // Буфер для хранения вертексов 16 LPD1RECT3DVERTEXBUFFER9 vertexBuffer; 17 18 window_init_params windowInitParams; 19 d3d_init_params d3dInitParams; 20 21 D3DXMATRIX viewMatrix; 22 D3DXMATRIX projectionMatrix; 23 24 public: 25 d3d_app(); 26 bool InitApp( 27 window_init_params windowParams, 28 d3d_init_params d3dParams); 29 30 LPDIRECT3DDEVICE9 D3DRenderingDevice(void); 31 32 LPDIRECT3DVERTEXBUFFER9 D3DVertexBuffer(void); 33 void D3DVertexBuffer( 34 LPDIRECT3DVERTEXBUFFER9 vertexBufferPointer); 35 36 DWORD RenderingDeviceClearFlags(void); 37 D3DC0L0R BackgroundSurfaceColor(void); 38 39 void ViewMatrix(D3DMATRIX newViewMatrix); 40 D3DMATRIX ViewMatrix(void); 41 42 void ProjectionMatrix( 43 D3DMATRIX newProjectionMatrix); 44 D3DMATRIX ProjectionMatrix(void); 45 46 /* Ниже перечислены функции, вызываемые платформой. 47 Из игры их вызывать не нужно. */ 48 friend INT WINAPI AppMain( 49 HINSTANCE hlnst, 50 HINSTANCE, 51 LPSTR, 52 INT); 53 friend LRESULT WINAPI AppMsgProc( 54 HWND hWnd, 55 UINT msg, 56 WPARAM wParam, 57 LPARAM lParam); 58 };
Готовимся создавать игры 379 Если вы посмотрите на private-раздел определения класса в листинге 14.1, то увидите, что в нем стало меньше элементов данных. Вместо отдельных элементов данных для каждого параметра инициализации окна и Direct3D теперь в классе d3d_app используются элементы-структуры. Эти элементы объявлены в строках 18-19. Одна из структур содержит все параметры инициализации окна, а вторая - все параметры инициализации Direct3D. Эти небольшие изменения избавили нас от огромного объема работы. Теперь мы можем добавлять в платформу параметры инициализации, добавляя их в структуры window_init_params и d3d_init_params. Это автоматически добавит их в класс d3d__app. Следующий шаг - сделать инициализацию структур window_init_ params и d3d_init_params автоматической. Было бы неплохо, если бы нам не приходилось добавлять новые процедуры инициализации в конструктор класса d3d_app при каждом добавлении новых элементов данных в структуры window_init_params и d3d_init_params. Как сделать инициализацию автоматической, показано в листинге 14.2. Листинг 14.2. Обновленный конструктор класса d3d_app 1 inline d3d_app::d3d_app() 2 { 3 direct3D = NULL; 4 d3dDevice = NULL; 5 vertexBuffer = NULL; 6 7 memset(SwindowInitParams,0,sizeof(windowInitParams)); 8 memset(&d3dInitParams,0,sizeof(d3dInitParams)); 9 10 applnitialized=false; 11) Теперь конструктор класса d3d_app вызывает функцию C++ memset () для инициализации структур window_init_params и d3d_init_j>arams. Эта функция заполняет нулями память, выделенную под структуры, причем размер структур определяется во время выполнения программы. Поэтому неважно, сколько новых элементов данных вы добавите в эти структуры - вызовы в строках 7-8 все равно проинициализируют все элементы структур нулями. Упростить перенос инициализации в классе d3d_app так же просто. Этим занимается метод InitApp (), код которого приведен в листинге 14.3. Метод InitApp () из листинга 14.3 копирует значения параметров инициализации в класс d3d_app с помощью простых операторов присваивания. Этот прием избавит вас от необходимости добавлять код в этот метод при добавлении новых элементов в структуры. Я модифицировал функции InitD3D () и WinMain () в файле PMD3D- Арр. срр, и они используют элементы структур window_init_j?arams и
380 Глава 14 d3d_init_j>arams. Теперь, чтобы добавить новый параметр инициализации, нужно выполнить следующие шаги: 1. Добавить новый элемент в структуру window_init_params и d3d_init_params. 2. Добавить в функцию d3d_app: : lnitD3D () или WinMain() код, использующий новый элемент структуры. Эта процедура намного проще, чем использовавшаяся ранее. Листинг 14.3. Новая версия метода d3d_app::lnitApp() 1 inline bool d3d_app::InitApp( 2 window_init_params windowParams, 3 d3d_init_params d3dParams) 4 { 5 // Задаем начальные параметры окна 6 windowInitParams = windowParams; 7 8 // Задаем начальные параметры D3D. 9 d3dInitParams = d3dParams; 10 11 applnitialized=true; 12 13 return(applnitialized); 14 } Добавление класса game Если вы будете писать игру с использованием платформы физического моделирования, вам, скорее всего, придется добавлять в нее сотни, а возможно, и тысячи функций. Кроме того, эти функции должны будут работать с большими объемами данных. Было бы удобно объединить доступ к данным и функциям. Кроме того, хорошо было бы, чтобы эти функции и данные были доступны из любой точки программы. Проще всего добиться этого, создав отдельный класс игры - game. Этот класс позволит вам обращаться к данным игры и ее функциям через одну точку, доступную во всех файлах программы. Определение класса game приведено в листинге 14.4. Листинг 14.4. Определение класса game 1 class game 2 { 3 public: 4 virtual bool OnAppLoad() = 0; 5 virtual bool PreD3DInitialization() = 0;
Готовимся создавать игры 381 6 virtual bool PostD3DInitialization() = 0; 7 virtual bool Gamelnitialization() = 0; 8 virtual bool HandleHessage( 9 HWND hWnd, 10 UINT msg, 11 WPARAM wParam, 12 LPARAM lParam) = 0; 13 virtual bool Processlnput()=0; 14 virtual bool InvalidateDeviceObjects()=0; 15 virtual bool RestoreDeviceObjects()=0; 16 virtual bool UpdateFrame() = 0; 17 virtual bool RenderFrame() = 0; 18 virtual bool GameCleanup() = 0; 19 }; В определении класса из листинга 14.4 содержатся прототипы требуемых платформой методов. Однако, поскольку это чисто виртуальные методы, для них нет определений. Указатель на объект класса game добавлен в класс d3d_app (строка 29 листинга 14.5): Листинг 14.5. Определение класса d3d_app 1 class d3d_app 2 { 3 private: 4 // Свойства приложения. 5 bool applnitialized; 6 7 // Свойства окна. 8 std::string windowTitle; 9 int defaultX, default?; 10 int defaultHeight,defaultWidth; 11 12 // Свойства D3D. 13 // Используется для создания D3DDevice 13 LPDIRECT3D9 direct3D; 14 LPDIRECT3DDEVICE9 d3dDevice; // Наше устройство рендеринга 15 // Буфер для хранения вертексов 16 LPDIRECT3DVERTEXBUFFER9 vertexBuffer; 17 // Свойства Directlnput 18 LPDIRECTINPUT8 directlnputDevice; 19 LPDIRECTINPUTDEVTCE8 keyboardDevice; 20 LPDIRECTINPUTDEVICE8 mouseDevice; 21 22 window_init_params windowInitParams; 23 d3d_init_params d3dInitParams; 24 direct_input_init_params directlnputParams; 25
382 Глава 14 26 D3DXMATRIX viewMatrix; 27 D3DXMATRIX projectionMatrix; 28 29 game *theGame; 30 31 bool viewMatrixDirty; 32 bool projectionMatrixDirty; 33 34 private: 35 bool CreateGameObject(void); 36 HRESULT InitD3D{ 37 HWND hWnd); 38 bool InitDirectlnput(); 39 VOID CleanupD3D( 40 bool timeToQuit); 41 VOID CleanupDireotlnput(void); 42 void SetPresentParameters( 43 D3DPRESENT_PARAMETERS *d3dpp); 44 VOID Render(); 45 46 public: 47 d3d_app(); 4в bool XnitApp< 49 window_init_params windowParams, 50 d3d_init_params d3dParams, 51 direct_input_init_params diParams); 52 53 LPDIRECT3DDEVICE9 D3DRenderingDevice(void); 54 LPDIRECTINPUT8 DirectlnputDevice(); 55 LPDIRECTINPUTDEVICE8 Keyboard(); 56 LPDIRECTINPUTDEVICE8 Mouse(); 57 58 LPDIRECT3DVERTEXBUFFER9 D3DVertexBu£fer(void); 59 void D3DVertexBuffer( 60 LPDIRECT3DVERTEXBUFFER9 vertexBufferPointer); 61 62 DWORD RenderingDeviceClearFlags(void); 63 D3DCOLOR BackgroundSurfaceColor(void); 64 65 void ViewMatrix(D3DMATRIX newViewMatrix); 66 D3DMATRIX ViewMatrix(void); 67 68 void ProjectionMatrix( 69 D3DMATRIX newProjectionMatrix); 70 D3DMATRIX PrоjectionMatrix(void); 71 72 game *Game(void); 73 74 /* Ниже перечислены функции, вызьтаемые платформой.
Готовимся создавать игры 383 75 Из игры их вызывать не нужно. */ 76 friend INT WINAPI AppMain( 77 HINSTANCE hlnst, 78 HINSTANCE, 79 LPSTR, 80 INT); 81 friend LRESOLT WINAPI AppMsgProc( 82 HWND hWnd, 83 OINT msg, 84 WPARAM wParam, 85 LPARAM lParam); 86 }; Обратите внимание на метод, объявленный в строке 72 листинга 14.5. Это метод Game(), возвращающий указатель на объект класса game. Вспомните, что платформа автоматически создает глобальную переменную theApp типа d3d_app. Эта переменная доступна в любом файле, в который включен файл PMFramework. h. Поэтому ваша программа сможет обращаться к объекту game через переменную theApp, например: theApp.Game(); Но, чтобы такой подход заработал, нужно создать класс, наследующий класс pmf ramework: : game. Кроме того, ваша программа должна использовать макрос CREATE_GAME_OBJECT () и передать ему имя вашего класса игры. Как это делается, демонстрирует листинг 14.6. Листинг 14.6. Создание и использование класса игры 1 class my_game : public game 2 { 3 private: 4 immersible_rigid_body allBalls[TOTAL_BALLS]; 5 6 public: 7 bool OnAppLoad(); 8 bool PreD3DInitialization(); 9 bool PostD3DInitialization(); 10 bool Gamelnitialization(); 11 bool HandleMessage( 12 HWND hWnd, 13 UINT msg, 14 WPARAM wParam, 15 LPARAM lParam); 16 bool UpdateFrame(); 17 bool RenderFrame(); 18 bool GameCleanupO ; 19 }; 20 21 CREATE GAME OBJECT(my game);
384 Глава 14 Этот пример класса игры использует прямое наследование от класса game. Если вы хотите увидеть реализации функций класса my_game, посмотрите их в файле FloatTest.cpp в папке \Source\Chapterl4\Ne- wFloat на компакт-диске, прилагающемся к книге. Программа в этой папке - это версия программы из главы 13 «Вода и волны», использующая новую версию платформы физического моделирования. Обратите внимание на вызов макроса CREATE_GAME_OB JECT () в строке 21 листинга 14.6. Этому макросу в качестве параметра передается имя класса, производного от класса game из платформы. В данном примере - это класс my_game. На самом деле макрос CREATE_GAME_OBJECT () создает в программе функцию CreateGameObjectO. Платформа автоматически вызывает эту функцию, чтобы выделить память под объект вашего класса игры. В данном примере память выделяется под объект класса my_game. Кроме того, она записывает в указатель в объекте класса d3d_app адрес, по которому расположен в памяти объект класса игры. Предупреждение При компиляции и компоновке игры, использующей платформу физического моделирования, могут появиться сообщения о том, что компоновщик не может найти функцию CreateGameObjectO . Это значит, что вы не вызвали макрос create_game_object () ни в одном из файлов программы. Класс my_game из листинга 14.6 содержит данные игры. Поскольку все служебные функции игры теперь являются методами класса ту_ дате, они могут напрямую обращаться к внутренним данным игры. Все это означает, что при написании игр, использующих платформу, нужно сделать следующее: 1. Создать класс, производный от pmframework: :game. 2. Написать в этом классе код требуемых функций. 3. Вызвать макрос CREATE GAME OBJECT (). Если выполнить эти действия, программа сможет обращаться к объекту игры и его методам из любой точки. Замечание Эти действия выполнены в файле FloatTest.cpp из папки \Sour- ce\Chapterl4\NewFloat на компакт-диске, прилагающемся к книге. Эффективное задание матриц преобразований Во всех программах, рассмотренных ранее в книге, выполнялось большое количество ненужных операций - матрицы отображения и проецирования в них обновлялись при рендеринге каждого кадра. Давайте избавимся от
Готовимся создавать игры 385 этих излишних операций и сделаем так, чтобы матрицы передавались Di- rect3D автоматически при их изменении. Чтобы сделать это, сначала нужно добавить несколько переменных состояния в класс d3d арр. Определение класса d3d_app приведено в листинге 14.7. Листинг 14.7. Определение класса d3d_app 1 class d3d_app 2 { 3 private: 4 // Свойства приложения. 5 bool applnitialized; 6 7 // Свойства окна. 8 std::string windowTitle; 9 int defaultX, defaultY; 10 int defaultHeight,defaultWidth; 11 12 // Свойства D3D. 13 // Используется для создания D3DDevice 13 LPDIRECT3D9 direct3D; 14 LPDIRECT3DDEVICE9 d3dDevice; // Наше устройство рендеринга 15 // Буфер для хранения вертексов 16 LPDIRECT3DVERTEXBUFFER9 vertexBuffer; 17 18 window_init_params windowInitParams; 19 d3d_init_params d3dlnitParams; 20 21 D3DXMATRIX viewMatrix; 22 D3DXMATRIX projectionMatrix; 23 24 game *theGame; 25 26 bool viewMatrixDirty; 27 bool projectionMatrixDirty; 28 29 private: 30 bool CreateGameObject(void); 31 32 public: 33 d3d_app(); 34 bool InitApp( 35 window_init_params windowParams, 36 d3d_init_params d3dParams); 37 38 LPDIRECT3DDEVICE9 D3DRenderingDevice(void); 39 40 LPDIRECT3DVERTEXBUFFER9 D3DVertexBuffer(void);
386 Глава 14 41 void D3DVertexBuffer( 42 LPDIRECT3DVERTEXBUFFER9 vertexBufferPointer); 43 44 DWORD RenderingDeviceClearFlags(void); 45 D3DC0L0R BackgroundSurfaceColor(void); 46 47 void ViewMatrix(D3DMATRIX newViewMatrix); 48 D3DMATRIX ViewMatrix(void); 49 50 void ProjectionMatrix( 51 D3DMATRIX newProjectionMatrix); 52 D3DMATRIX ProjectionMatrix(void); 53 54 game *Game(void); 55 56 /* Ниже перечислены функции, вызываемые платформой. 57 Из игры их вызывать не нужно. */ 58 friend INT WINAPI AppMain( 59 HINSTANCE hlnst, 60 HINSTANCE, 61 LPSTR, 62 INT); €3 friend LRESDLT WINAPI AppMsgProc( 64 HWND hWnd, 65 UINT msg, 66 WPARAM wParam, 67 LPARAM lParam); 68 ); В этой версии определения класса d3d_app объявлены две переменные состояния (строки 26-27). Это булевы переменные, которые устанавливаются в true, если соответствующие матрицы изменились после того, как был отрисован предыдущий кадр. Функция Render () платформы, находящаяся в файле PMD3DApp. срр, теперь объявлена как дружественная к классу d3d_app. Это позволяет ей напрямую обращаться к элементам viewMatrixDirty и projection- MatrixDirty и проверять, изменились ли матрицы, и нужно ли передавать их Direct3D. Платформа будет делать это автоматически. Новая версия функции Render () приведена в листинге 14.8. В строке 13 листинга 14.8 функция Render () проверяет, изменилась ли матрица отображения. Если да, то функция передает новую матрицу Direct3D в строках 16-18. В строке 19 функция сбрасывает переменную viewMatrixDirty в значение false, чтобы матрица не передавалась DirectSD, пока в ней снова не произойдут изменения. Точно так же функция Render () проверяет матрицу проецирования в строке 22. Если она изменилась, она передается Direct3D в строках 25-27. Затем переменная pro jectionMatrixDirty сбрасывается в значение false в строке 28.
Готовимся создавать игры 387 Листинг 14.8. Функция RenderQ 1 VOID Render() 2 { 3 // Заполняем буфер черным цветом 4 theApp.D3DRenderingDevice()->Clear( 5 О,NULL, 6 theApp.RenderingDeviceClearFlags(), 7 theApp.BackgroundSurfaceColor(), 8 1.0f,0); 9 10 // Начинаем рендеринг сцены 11 i f(SUCCEEDED(theApp.D3DRenderingDevice()->BeginScene())) 12 { 13 if (theApp.viewMatrixDirty) 14 { 15 // Задаем матрицу отображения. 16 theApp.D3DRenderingDevice()->SetTransform( 17 D3DTS_VIEW, 18 &theApp.VlewMatrix()) ; 19 theApp.viewMatrixDirty = false; 20 } 21 22 if (theApp.projectionMatrixDirty) 23 { 24 // Задаем матрицу проецирования. 25 theApp.D3DRenderingDevice()->SetTransform( 26 D3DTS_PROJECTION, 27 &theApp.ProjectionMatrix()); 28 theApp.projectionMatrixDirty = false; 29 } 30 31 theApp.Game()->RenderFrame(); 32 33 // Завершаем рендеринг1 сцены 34 theApp.D3DRenderingDevice()->EndScene(); 35 } 36 37 // Выводим содержимое буфера на экран 38 theApp.D3DRenderingDevice()->Present(NULL,NULL,NULL,NULL); 39 } Чтобы все это работало, матрицы отображения и проецирования должны помечаться как изменившиеся при каждом их изменении. Эта операция выполняется в функциях ViewMatrix () и Pro jectionMatrix (), код которых приведен в листинге 14.9.
388 Глава 14 Листинг 14.9. Функции, помечающие матрицы отображения и проецирования 1 inline void d3d_app::ViewMatrix( 2 D3DMATRIX newViewMatrix) 3 { 4 viewMatrix=newViewMatrix; 5 viewMatrixDirty = true; 6 } 7 8 inline void d3d_app::ProjectionMatrix( 9 D3DMATRIX newProjectionMatrix) 10 { 11 projectionMatrix=newProjectionMatrix; 12 projectionMatrixDirty = true; 13) Функции из листинга 14.9 используются для задания матриц отображения и проецирования. Они устанавливают в значение true переменные состояния при изменении соответствующих матриц. Это все, что нужно для автоматического обновления матриц отображения и проецирования. Восстановление потерянных объектов устройств В том виде, в каком платформа существует сейчас, она обладает существенным недостатком. Иногда Direct3D может терять управление графической памятью, если пользователь изменяет размер окна или переключается на другую графическую программу. Рассмотренные ранее в этой книге примеры программ настолько малы, что в них такие события маловероятны. Однако в настоящих играх потеря объектов устройств происходит довольно часто. Когда игра теряет объекты устройств, то могут теряться вертексные буферы, текстуры, цепочки обмена, поверхности рендеринга и стенсиль- ные ресурсы. Однако они теряются только в том случае, если располагаются в графической памяти. Все, что размещено в D3DPOOL_MANAGED или D3D_POOL_SYSTEMMEM, находится в безопасности. Чтобы восстановить управление памятью и воссоздать объекты устройств, игра должна выполнить следующие действия: 1. Освободить всю видеопамять, которую программа выделила в классе памяти D3DP0OL_DEFAULT. Сюда входят цепочки обмена, созданные функцией CreateAdditionalSwapChain (), поверхности рендеринга, созданные функцией CreateRenderTarget (), и стенсильные ресурсы, выделенные функцией CreateDepth- StencilSurface().
Готовимся создавать игры 389 2. Опросить устройство, чтобы определить, может ли программа выполнить сброс его состояния. 3. Если сброс можно выполнить, сделать это с помощью функции Reset (). 4. Заново настроить устройство. 5. Создать или загрузить снова все, что было освобождено в пункте 1. Я модифицировал платформу так, чтобы она выполняла большую часть этих действий за вас. Теперь в классе game есть два специальных метода. Первый из них - метод RestoreDeviceObjects (). Если вашей игре нужно создавать объекты устройств, поместите код их создания в метод RestoreDeviceObjects (). При начальной инициализации программы вызовите метод RestoreDeviceObjects () из функции Gamelnitializa- tion (). После этого платформа будет автоматически вызывать метод RestoreDeviceObjects (), если ей нужно будет выполнить пункт 5. Второй новый метод класса game называется InvalidateDeviceOb- jects (). Поместите код освобождения объектов устройств, который нужен вашей игре, в этот метод. При завершении выполнения игры вызовите метод InvalidateDeviceObjects() из функции GameClea- nup(). Платформа автоматически вызывает метод InvalidateDeviceObjects (), когда ей нужно выполнить пункт 1. Если вы используете вертексный буфер, объявленный в классе d3d_app, то функции InvalidateDeviceObjects () не нужно помечать его как потерянный. Платформа сделает это за вас. Однако функции RestoreDeviceObjects () придется восстанавливать вертексный буфер с нуля. Вот, собственно говоря, и все. Для программ, которые мы будем создавать далее в этой книге, вам не понадобится добавлять много кода в функции RestoreDeviceObjects() и InvalidateDeviceObjects(). Единственный класс в платформе, использующий текстуры - это класс mesh. Но он хранит свои текстуры и данные о материалах в системной памяти, и это гарантирует, что они не будут потеряны. Единственное, что должны делать функции RestoreDeviceObjects () и InvalidateDeviceObjects () в программах из этой книги, - возвращать значение true. На компакт-диске, поставляющемся с книгой, есть версия программы из главы 13, использующая новую версию платформы. Она находится в папке \Source\Chapterl4\NewFloat2. Переопределение твердых тел с помощью материальных точек Возможно, читая предыдущие главы, вы заметили, что значительная часть функциональности классов твердых тел и материальных точек одинакова. Собственно говоря, рассматривая динамику поступательного
390 Глава 14 движения, физики часто считают твердые тела материальными точками. В наших задачах мы можем считать, что твердое тело - это материальная точка, снабженная дополнительной функциональностью, касающейся динамики вращения. Платформа физического моделирования должна учитывать эту схожесть объектов. Чтобы добиться этого, проще всего воспользоваться механизмом наследования языка C++. Давайте перепишем класс rigid_ body таким образом, чтобы он стал производным от класса point_mass. Преимущество такого класса будет заключаться в исчезновении повторяющихся методов и элементов данных. При этом классы будет проще модифицировать и поддерживать при написании игр с использованием платформы физического моделирования. Такое обновление требует внесения некоторых изменений в класс ро- int_mass_base. Новая версия этого класса показана в листинге 14.10. Листинг 14.10. Определение обновленного класса point_mass_base 1 class point_mass_base 2 { 3 private: 4 scalar mass; 5 vector_3d centerOfMassLocation; 6 vector_3d linearVelocity; 7 vector_3d linearAcceleration; 8 force constantForce; 9 force impulseForce; 10 11 scalar boundingSphereRadius; 12 scalar coefficientOfRestitution; 13 14 bool isImmovable; 15 16 public: 17 point_mass_base(); 18 19 void Mass( 20 scalar massValue) ; 21 scalar Mass(void); 22 23 void Location( 24 vector_3d locationCenterOfMass); 25 vector_3d Location(void); 26 27 void LinearVelocity( 28 vector_3d newVelocity); 29 vector_3d LinearVelocity(void);
Готовимся создавать игры 391 30 31 void LinearAcceleration{ 32 vector_3d newAcceleration); 33 vector_3d LinearAcceleration(void); 34 35 void ConstantForce( 36 force sumConstantForces); 37 force ConstantForce(void); 38 39 void ImpulseForce( 40 force sumlmpulseForces); 41 force ImpulseForce(void); 42 43 void BoundingSphereRadius( 44 scalar sphereRadius); 45 scalar BoundingSphereRadius(void); 46 47 void CoefficientOfRestitution(scalar elasticity); 48 scalar CoefficientOfRestitution(void); 49 50 void IsImmovable( 51 bool isMassImmovable); 52 bool Islmmovable(void); 53 54 virtual bool Update( 55 scalar changelnTime); 56 }; Самое важное изменение в классе point_mass_base — он теперь использует объекты класса force, а не векторы для хранения постоянно действующих и импульсных сил. Соответственно, изменились и методы класса. Еще одно изменение - перегруженные методы Elasticity () теперь называются Coef ficientOfRestitution(). Это позволяет достичь единообразия в задании упругости материальных точек и твердых тел. Предупреждение Эти изменения означают, что новая версия платформы физического моделирования не обеспечивает обратной совместимости с программами примеров из предыдущих глав. Определение новой версии класса rigid_body приведено в листинге 14.11.
392 Глава 14 Листинг 14.11. Определение обновленного класса rigid_body 1 class rigid_body : public point_mass 2 { 3 private: 4 // Свойства вращательного движения 5 angle_set_3d currentOrientation; 6 vector_3d angularVelocity; 7 vector_3d angularAcceleration; 8 vector_3d rotationallnertia; 9 vector_3d torque; 10 11 public: 12 rigid_body(void); 13 14 virtual void CurrentOrientation( 15 angle_set_3d newOrientation); 16 virtual angle_set_3d CurrentOrientation(void); 17 18 virtual void AngularVelocity( 19 vector_3d newAngularVelocity); 20 virtual vector_3d AngularVelocity(void); 21 22 virtual void AngularAcceleration( 23 vector_3d newAngularAcceleration); 24 virtual vector_3d AngularAcceleration(void); 25 26 virtual void Rotationallnertia( 27 vector_3d inertiaValue); 28 virtual vector_3d Rotationallnertia(void); 29 30 virtual void Torque( 31 vector_3d torqueValue); 32 virtual vector_3d Torque(void); 33 34 virtual bool Update( 35 scalar changelnTime); 36 ); Как видно из листинга 14.11, теперь определение класса rigidjbody намного компактнее. Элементы данных и методы, относившиеся к поступательному движению, исчезли, как и сетчатая модель и метод Load- Mesh (). Все они унаследованы от класса point_mass. Кроме того, заметьте, что теперь в классе rigid_body нет метода Render (). Он не нужен. Все, что он делал, - выполнял рендеринг сетчатой модели объекта соответственно глобальной матрице. Но метод Render () в классе point_mass уже делает это. Поэтому в классе rigid_body теперь используется метод Render () из класса point_mass.
Готовимся создавать игры 393 Все эти изменения оказывают некоторое влияние на платформу физического моделирования. Большинство их последствий незначительны, и мы не будем на них останавливаться. Но прежде чем мы двинемся дальше, взгляните на изменившийся метод rigid_body: : Update (), код которого приведен в листинге 14.12. Листинг 14.12. Метод rigid_body::Update() 1 bool rigid_body::Update( scalar changelnTime) 2 { 3 // 4 // Начинаем просчет вращательной динамики. 5 // 6 7 // Используем импульсные силы для вычисления вращающего момента. 8 torque = 9 ImpulseForce().ApplicationPoint().Cross( 10 ImpulseForce().Force()); 11 12 /* По вращающему моменту и инерции находим угловое ускорение.*/ 13 angularAcceleration.X( 14 torque.X()/rotationalInertia.X()); 15 angularAcceleration.Y( 16 torque.Y()/rotationalInertia.Y()); 17 angularAcceleration.Z( 18 torque.Z()/rotationalInertia.Z()); 19 20 /* Изменяем угловую скорость соответственно угловому ускорению 21 acceleration. */ 22 angularVelocity += angularAcceleration * changelnTime; 23 24 // 25 // Используем угловое ускорение, чтобы найти углы вращения. 26 // 27 currentOrientation.XAngle( 28 currentOrientation.XAngle() + 29 angularVelocity.X() * changelnTime); 30 currentOrientation.YAngle( 31 currentOrientation.YAngle() + 32 angularVelocity.Y() * changelnTime); 33 currentOrientation.ZAngle( 34 currentOrientation.ZAngle() + 35 angularVelocity.Z() * changelnTime); 36 37 // 38 // Закончили просчет вращательной динамики. 39 // 40
394 Глава 14 41 // Строим матрицы вращения для каждой из осей. 42 D3DXMATRIX rotationX, rotationY, rotationZ; 43 D3DXMatrixRotationX(SrotationX,current0rientation.XAngle()); 44 D3DXMatrixRotationY(SrotationY,current0rientation.YAngle<)); 45 D3DXMatrixRotationZ(SrotationZ,currentOrientation.Z Angle()) ; 46 47 D3DXMATRIX totalRotations; 48 49 // Перемножаем их в глобальную матрицу. 50 D3DXMatrixMultiply( 51 &totalRotations, 52 SrotationX, 53 irotationY); 54 D3DXMatrixMultiply( 55 StotalRotations, 56 StotalRotations, 57 SrotationZ); 58 59 // Просчитываем поступательную динамику. 60 point_mass_base::Update(changelnTime); 61 62 // Создаем матрицу перемещения. 63 D3DXMATRIX totalTranslation; 64 D3DXMatrixTranslation( 65 &totalTranslation, 66 Location().X(), 67 Location().Y(), 68 Location() .Z()) ; 69 70 /* Совмещаем матрицы вращения и перемещения в глобальной 71 матрице. */ 72 D3DXMatrixMultiply( 73 SworldMatrix, 74 &totalRotations, 75 StotalTranslation); 76 77 /* Импульсные силы приложены. Сбрасываем их в ноль. 78 Заметьте, что point_mass_base::Update() уже сбросил 79 в ноль направление и величину сил. Все, что нужно сделать - 80 сбросить в ноль точку приложения сил.*/ 81 ImpulseForce().ApplicationPoint(vector_3d@.0,0.0,0.0)); 82 83 return(true); 84 ) Эта версия метода Update () весьма сильно отличается от использовавшейся ранее версии. Например, она сначала просчитывает вращательную динамику, а не поступательную. Вы узнаете причину этого изменения из
Готовимся создавать игры 395 строки 60 листинга 14.12. Из метода rigid_body: :Update () вызывается метод point_mass_base::Update(). Метод point_mass_base::Update () сбрасывает в 0 величину и вектор направления импульсной силы. Если бы вызов метода point_mass_base: : Update () был выполнен до просчета вращательной динамики, то этот метод сбросил бы их в 0 до просчета, и результаты моделирования были бы неверными. Поэтому вращательная динамика просчитывается первой. Кроме того, важно, что в листинге 14.12 вызывается метод ро- int_mass_base::Update(), а не point_mass::Update(). В методе point_mass : : Update () вызывается метод point_mass_base: : Update (), и полученное перемещение сохраняется в глобальной матрице. Но при моделировании объекта класса rigid_body сначала нужно просчитать все повороты, а затем помещать в глобальную матрицу перемещения. Поэтому для просчета перемещений в строке 60 вызывается метод point_mass_base: : Update (). Затем создается матрица перемещений. После этого матрицы поворота и перемещения объединяются в строках 72-75. Это делается таким образом, что повороты будут выполнены до перемещения. Замечание На компакт-диске, прилагающемся к книге, обновленная версия платформы содержится в папке Source\Chapterl4\Framework. Центр масс и точка начала координат сетчатой модели Все программы, которые мы создали в предыдущих главах, подразумевали, что точка начала координат сетчатой модели объекта совпадает с его центром масс. В реальных играх это часто бывает не так. Поэтому в платформу нужно внести некоторые изменения, чтобы игры могли задавать эту точку независимо от центра масс. В листинге 14.13 приведено определение новой версии класса point_mass, в которой можно отдельно задавать координаты центра масс и начала координат сетчатой модели. Чтобы физика трехмерных объектов работала правильно, позиции всех материальных точек и твердых тел в игре нужно отслеживать по их центрам масс. Точки начал координат сетчатых моделей используются только для рендеринга этих моделей. Версия класса point_mass из листинга 14.13 содержит private-элемент данных meshOrigin. Кроме того, в классе есть методы чтения и установки значения этого элемента, в котором хранятся координаты точки начала координат сетчатой модели. Прототипы этих функций содержатся в строках 17-19. Вектор meshOrigin должен задаваться относительно центра масс объекта. Если бы он задавался относительно начала глобальной системы координат игры, то поведение его было бы невозможно отследить.
396 Глава 14 Листинг 14.13. Класс pointjnass с независимой точкой начала координат сетчатой модели 1 class pointjnass : public point_mass_base 2 { 3 private: 4 mesh objectMesh; 5 vector_3d meshOrigin; 6 7 protected: 8 D3DXMATRIX worldMatrix; 9 10 public: 11 bool LoadMesh( 12 std::string meshFileName); 13 14 void ShareMesh( 15 point_mass SsourceMass); 16 17 void MeshOriginOffset( 18 vector_3d offset); 19 vector_3d MeshOriginOffset(void); 20 21 bool Update( 22 scalar changeInTime); 23 bool Render(void); 24 ); Элемент meshOrigin используется в методе Render () класса po- int_mass. Код этого метода приведен в листинге 14.14. В новой версии метода point_mass: : Render (), приведенной в листинге 14.14, после помещения всех поворотов и перемещений в глобальную матрицу в нее помещается дополнительное перемещение. В строках 11-16 видно, что метод создает матрицу перемещения, учитывающую смещение точки начала координат сетчатой модели. В строка 17-20 это смещение учитывается в глобальной матрице. Затем эта матрица используется для рендеринга сетчатой модели. По умолчанию конструктор класса vector_3d инициализирует элементы данных объектов нулями. Поэтому, если у какого-то объекта центр масс совпадает с началом координат сетчатой модели, то задавать это начало координат специально нет необходимости. При этом нулевая величина вектора приведет к тому, что код в строках 11-20 листинга 14.14 не делает вообще ничего. Вспомните, что класс rigid_body теперь является производным от класса point_mass. Соответственно, поскольку смещение точки начала координат сетчатой модели добавлено в класс point_mass, оно присутствует и в классе rigid_body.
Готовимся создавать игры 397 Листинг 14.14. Новая версия метода point_mass::Render() 1 bool point_mass::Render(void) 2 { 3 // Сохраняем глобальную матрицу. 4 D3DXMATRIX saveWorldMatrix; 5 theApp. D3DRenderingDevi.ce () ->GetTransf orm( 6 D3DTS_WORLD, 7 SsaveWorldMatrix) ; 8 9 // Добавляем перемещение, соответствующее смещению 10 // точки начала координат сетчатой модели. 11 D3DXMATRIX meshOffsetTranslation; 12 D3DXMatrixTranslation( 13 fimeshOffsetTranslation, 14 Mesh0rigin0ffset().X(), 15 Mesh0rigin0ffset().Y(), 16 MeshOriginOffset().Z()); 17 D3DXMatrixMultiply( 18 SworldMatrix, 19 SworldMatrix, 20 SmeshOffsetTranslation); 21 22 // Применяем глобальную матрицу х данному объекту. 23 theApp.D3DRenderingDevice()->SetTransform ( 24 D3DTSJWORLD,SworldMatrix); 25 26 // Отрисовываем объект с преобразованиями. 27 bool renderedOK=objectMesh.Render(); 28 29 // Восстанавливаем глобальную матрицу. 30 theApp.D3DRenderingDevice()->SetTransform( 31 D3DTS_W0RLD, 32 SsaveWorldMatrix); 33 34 return (renderedOK); 35} Замечание На компакт-диске, прилагающемся к книге, обновленная версия платформы содержится в папке Source\Chapterl4\Framework.
398 Глава 14 Введение в Directlnput Все предшествующие программы из этой книги не требовали ввода данных. Но игры должны реагировать на действия пользователя. В нескольких последних главах книги мы создадим похожие на игры программы, в которых используются рассмотренные нами физические модели. Эти программы интерактивны, и прежде чем начать изучать их, нам нужно разобраться с основами Directlnput. Directlnput не использует стандартные для Windows процедуры обработки действий пользователя. Это необходимо, поскольку стандартные процедуры слишком медлительны, чтобы подходить для игр. Directlnput непосредственно обращается к драйверам устройств ввода. Он может обеспечить реагирование на ввод в реальном времени, которое требуется для быстрых динамичных игр. Поскольку Directlnput обращается непосредственно к драйверам устройств, он может обеспечить два режима ввода. Первый - небуфери- зованный ввод (unbuffered input) - просто сообщает состояние устройства ввода в момент запроса. Он не передает информации об изменениях этого состояния. Проще говоря, если раньше кнопка была нажата, а сейчас - нет, то небуферизованный вывод не сохранит информации об этом переходе. Он только сообщит, что в данный момент кнопка не нажата. Если ваша программа использует небуферизованный ввод, она должна сама заниматься сохранением предыдущих состояний. Это значит, что вы получаете очень быстрый ввод, но должны писать довольно сложные процедуры его обработки в программе. Второй режим ввода, поддерживаемый Directlnput - это буферизованный ввод (buffered input). Этот режим похож на режим работы сообщений Windows. Когда состояние устройства изменяется, например, пользователь нажимает клавишу на клавиатуре, Directlnput генерирует соответствующее событие и помещает его в буфер. Программа считывает содержимое буфера, когда ей это нужно. Directlnput различает три разновидности устройств ввода: клавиатуры, мыши и джойстики. Это не значит, что ваша игра не сможет поддерживать более экзотические устройства, вроде геймпадов или рулей. С точки зрения Directlnput любое устройство ввода, которое не является клавиатурой или мышью, является джойстиком. Соответственно, ваши игры могут поддерживать геймпады и рули, выдавая их за джойстики. Directlnput должен получить контроль над устройствами ввода, прежде чем использующая его программа сможет получать от этих устройств информацию. Лучше всего делать это, когда программа активна и когда с этих устройств поступают данные.
Готовимся создавать игры 399 Компиляция программ, использующих Directlnput При компиляции программ, в которых вызываются функции Directlnput, Visual Studio требует подключения библиотеки dinputS. lib в поле Additional Dependencies. Если вы используете Visual Studio 6, откройте меню Project и выберите в нем пункт Settings. Откроется диалоговое окно Project Settings. Перейдите в нем на вкладку Link. Найдите текстовое поле Object/Library Modules. В этом поле введите имена модулей, которые вам нужны. Для большинства приложений, использующих Directlnput, достаточно ввести: dinput8.lib Если вы используете Visual Studio .NET, щелкните правой кнопкой на значке проекта в окне Solution Explorer. Из появившегося контекстного меню выберите пункт Properties. В левой части появившегося окна щелкните на папке Linker, а затем - на пункте Input. В текстовом поле Additional Dependencies введите имя библиотеки: dinput8.lib Для вашего удобства список нужных библиотек есть в файле Additional- Dependencies . txt в папке Source на прилагающемся к книге компакт-диске. Просто скопируйте содержимое этого файла в поле Additional Dependencies, если не хотите набирать имена библиотек вручную. Инициализация Directlnput Чтобы получить доступ к устройствам ввода, игра должна проинициали- зировать как Directlnput, так и сами эти устройства. Это делается приблизительно так: 1. Создайте интерфейс Directlnput, вызвав функцию DirectInput8Create(). 2. Перечислите подключенные устройства, вызвав метод IDirectInput8::EnumDevices(). 3. Создайте интерфейсы отдельных устройств, вызвав метод IDirectInput8::CreateDevice(). 4. Выясните, какие органы управления (кнопки, ползунки, колесики прокрутки и так далее) есть на каждом устройстве, вызвав метод IDirectInputDevice8::EnumObjects(). 5. Задайте формат данных для каждого устройства, вызвав метод IDirectInputDevice8::SetDataFormat(). 6. Если игра должна поддерживать буферизованный ввод, задайте размер буфера, вызвав метод IDirectInputDevice8::SetProperty().
400 Глава 14 Чтобы упростить все это, я включил некоторые из этих шагов в платформу физического моделирования. Платформа автоматически создаст для вас интерфейс Directlnput. Если вы прикажете ей, она создаст интерфейсы для клавиатуры и мыши. Замечание Вы, вероятно, удивляетесь, почему я перешел с DirectX 9 на DirectX 8. Дело в том, что DirectX 9 не добавил никакой функциональности в Directlnput, поэтому номер версии интерфейса Directlnput в DirectX 9 все равно 8. Запутались? Не вы первые. Инициализация клавиатуры и мыши в платформе выполняется в методе OnAppLoad() класса игры, как видно из листинга 14.15. Листинг 14.15. Выбор устройств ввода 1 bool my_game::OnAppLoad{) 2 { 3 // Инициализация параметров окна. 4 window_init_params windowParams; 5 windowParams.appWindowTitle="DirectInput and Camera Test"; 6 windowParams.defaultX=100; 7 windowParams.defaultY=l0 0; 8 windowParams.defaultHeight=500; 9 windowParams.defaultWidth=500; 10 11 // Инициализация параметров Direct3D. 12 d3d_init_params d3dParams; 13 d3dParams.renderingDeviceClearFlags = 14 D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER; 15 d3dParams.surfaceBackgroundColor = D3DCOLOR_XRGB@,75,200); 16 d3dParams.enableAutoDepthStencil = true; 17 d3dParams.autoDepthStencilFormat = D3DFMT_D16; 18 d3dParams.enableD3DLighting = false; 19 20 // Инициализация параметров Directlnput. 21 direct_input_init_params diParams; 22 diParams.supportsKeyboard = true; 23 diParams.reguiresKeyboard = true; 24 diParams.supportsMouse = true; 25 diParams.requiresMouse = false; 26 27 // Этот вызов ДОЛЖЕН присутствовать в этой функции. 28 theApp.InitApp(windowParams,d3dParams,diParams); 29 30 return (true); 31 >
Готовимся создавать игры 401 В строках 21-25 листинга 14.15 выполняется инициализация новых элементов класса d3d_app. Теперь в этом классе содержится структура типа direct_input_init_params. Определение этой структуры приведено в листинге 14.16. Листинг 14.16. Структура direct_input_init_params 1 struct direct_input_init_params 2 { 3 bool supportsKeyboard; 4 bool requiresKeyboard; 5 bool supportsMouse; 6 bool requiresMouse; 7 }; Эта простая структура позволяет игре сообщить платформе, что она поддерживает клавиатуру, а так же необходима ли ей клавиатура. Если сбросить переменную requiresKeyboard в значение false, то платформа сочтет, что игра использует клавиатуру, но может без нее обойтись. Выбор мыши выполняется точно так же. Если переменная supportsMouse сброшена в false, значит, игре не нужна мышь. Если мышь используется, то supportsMouse нужно установить в true, а если мышь необходима, то кроме этого нужно установить в true и переменную requiresMouse. Как видно из листинга 14.15, значения элементов структуры задаются в методе OnAppLoad (). Я добавил новый элемент типа direct_input_ init_params в метод d3d_app: : InitAppO. Теперь метод d3d_ арр: : InitApp () копирует содержимое своего третьего параметра в элемент данных directlnputParams класса d3d_app. Содержимое этого элемента данных используется новым методом InitDirectlnput (), код которого приведен в листинге 14.17. Листинг 14.17. Инициализация Directlnput 1 bool d3d_app::InitDirectlnput() 2 { 3 bool noError = true; 4 5 // Создаем устройство Directlnput. 6 HRESULT result = 7 DirectInput8Create( 8 GetModuleHandle(NULL), 9 DIRECTINPUT_VERSION, 10 IID_IDirectInput8, 11 (VOID**)SdirectlnputDevice, NULL); 12 13 noError = (result—DI OK) ;
402 Глава 14 14 15 // Если игра поддерживает клавиатуру... 16 if ((noError) && (directlnputParams.supportsKeyboard)) 17 { 18 // Создаем устройство клавиатуры. 19 result = directInputDevice->CreateDevice( 2 0 GUID_SysKeyboard, 21 SkeyboardDevice, 22 NULL); 23 24 // Бели произошла ошибка, а клавиатура необходима игре... 25 if {(result !=DI_OK) && 26 (directlnputParams.requiresKeyboard)) 27 { 28 noError = false; 29 } 30 } 31 32 // Если игра поддерживает мышь... 33 if ((noError) && (directlnputParams.supportsMouse)) 34 { 35 // Создаем устройство мыши. 36 result = directInputDevice->CreateDevice( 37 GUID_SysMouse, 38 SmouseDevice, 39 NULL); 40 41 // Если произошла ошибка, а мышь необходима игре... 42 if ((result !=DI_OK) && (directlnputParams.requiresMouse)) 43 { 44 noError = false; 45 ) 46 } 47 48 return (noError); 49 } В листинге 14.17 приведен код нового метода из платформы. Этот код находится в файле PMD3DApp. срр. Метод InitDirectlnput () вызывает функцию DirectInput8Create(), чтобы создать интерфейс IDirectInput8. Параметры функции DirectInput8Create () объяснены в таблице 14.1. Четвертый параметр функции DirectInput8Create () - это адрес переменной-указателя. Именно в этом параметре DirectInput8Crea- te () сохраняет указатель на интерфейс IDirectlnpute. Теперь в классе платформы d3d_app есть указатель на интерфейс IDirectlnpute - это переменная из строки 11 листинга 14.17. Игра может обратиться к интерфейсу IDirectlnpute в любой момент, вызвав метод d3d_app: : Di- rectlnputDevice().
Готовимся создавать игры 403 Таблица 14.1. Параметры функции Directlnput8Create() Имя параметра Описание hinst dwVersion riidltf ppvOut punkOuter Дескриптор текущего модуля. Просто присвойте этому параметру значение, возвращаемое функцией GetModuleHandle(NULL) Присвойте этому параметру значение DIRECTINPUT_VERSION Идентификатор интерфейса, позволяющий использовать кодовые таблицы ANSI или Unicode. Я рекомендую присвоить ему значение HD_iDirectlnput8, чтобы использовались кодовые таблицы, заданные в настройках компилятора. Это сильно упрощает жизнь Адрес, по которому находится указатель на интерфейс. Именно сюда функция Directlnput8Create{) выводит адрес такого указателя для вашей программы Указатель, используемый для особо изощренной возможности СОМ-объектов, называемой агрегированием (aggregation). Если вы не знаете, что это такое, считайте, что вам повезло. Установите этот параметр в значение NULL и забудьте о нем. В любом случае, вы не будете его использовать - этот механизм в СОМ оооочень медленно работает, и использовать его в играх невозможно Замечание Поскольку теперь платформа содержит функции, использующие дополнительные компоненты DirectX, в частности, Directlnput, я внес в нее некоторые изменения. Теперь большинство функций в файле PMD3DApp.opp являются private-элементами класса d3d_app. Именно поэтому функция InitDirectlnput () включена в класс d3d_app. Так мы получим более удобную структуру инициализации дополнительных компонентов DirectX. Если создание интерфейса IDirectInput8 прошло без ошибок, метод InitDirectlnput () проверяет содержимое элемента данных directln- putParams, чтобы выяснить, требуется ли поддержка клавиатуры. Если да, то метод создает интерфейс IDirectInputDevice8 для клавиатуры. Если создание интерфейса для клавиатуры привело к ошибке, а клавиатура необходима игре, то метод InitDirectlnput () возвращает ошибку. Если же без клавиатуры можно обойтись, то метод продолжает выполняться, как ни в чем не бывало. Точно так же создается интерфейс IDirectInputDevice8 для мыши.
404 Глава 14 Теперь, когда у игры есть указатели на интерфейсы для устройств ввода, ей необходимо задать форматы получаемых от этих интерфейсов данных. Кроме того, если игра использует буферизованный ввод, она должна задать емкости буферов. Обе эти операции выполняются в методе Gamelnitialization () класса игры или в методе, вызываемом из этого метода. Код такого метода приведен в листинге 14.18. Листинг 14.18. Завершение инициализации устройств ввода 1 void my_game::SetupInputDevices() 2 { 3 DIPROPDWORD dipdw; 4 5 if (theApp.Keyboard()) 6 { 7 theApp.Keyboard()->SetDataFormat(&c_dfDIKeyboard); 8 9 dipdw.diph.dwSize = sizeof(DIPROPDWORD); 10 dipdw.diph.dwHeaderSize = sizeof(DIPROPHEADER); 11 dipdw.diph.dwObj = 0; 12 dipdw.diph.dwHow = DIPHJDEVICE; 13 dipdw.dwData = KEYBOARD_BUFFER_SIZE; 14 15 /* В этой примере ошибки, возвращаемые функцией, 16 никак не обрабатываются. Если произошла ошибка, значит, 17 с клавиатурой серьезные проблемы. Игра должна сообщить 18 об этом игроку и аккуратно завершиться. */ 19 theApp.Keyboard()->SetProperty( 20 DIPROP_BOFFERSIZE, 21 Sdipdw.diph); 22 } 23 24 if (theApp.Mouse()) 25 { 2 6 theApp.Mouse()->SetDataFormat(&c_dfDIMouse); 27 28 dipdw.diph.dwSize = sizeof(DIPROPDWORD); 29 dipdw.diph.dwHeaderSize = sizeof(DIPROPHEADER); 30 dipdw.diph.dwObj = 0; 31 dipdw.diph.dwHow = DIPH_DEVICE; 32 dipdw.dwData = MOUSE_BUFFER_SIZE; 33 34 /* В этом примере ошибки, возвращаемые функцией, 35 никак не обрабатываются. Если произошла ошибка, значит, 36 с мышью серьезные проблемы. Игра должна сообщить 37 об этом игроку и аккуратно завершиться. */ 38 theApp.Mouse()->SetProperty( 39 DIPROP_BUFFERSIZE, 40 &dipdw.diph); 41 > 42 }
Готовимся создавать игры 405 В листинге 14.18 приведен код метода, завершающего инициализацию клавиатуры и мыши. Этот метод должен вызываться из метода Gamelni- tialization (). Метод SetupInputDevices () задает формат данных для клавиатуры, вызывая метод IDirectInputDevice8: : SetDataFormat (). Он передает методу SetDataFormat () глобальную переменную, определенную в Directlnput. Эта глобальная переменная - одна из набора, определенного в Directlnput для настройки форматов данных устройств ввода. Эти переменные описаны в таблице 14.2. Таблица 14.2. Форматы данных устройств ввода Имя переменной Описание c_dfDlKeyboard Стандартный формат данных для клавиатуры. Создает массив из 256 байтов - по одному на каждую клавишу клавиатуры и еще несколько c_dfDlMouse Стандартный формат данных для мыши. Когда программа получает данные от мыши, она помещает их в структуру dimousestate, содержащую 4 байта, описывающую состояние кнопок мыши c_dfDlMouse2 Расширенный формат данных для мыши. Когда программа получает данные от мыши, она помещает их в структуру dimousestate2, содержащую 8 байтов, описывающую состояние кнопок мыши c_dfDUoystick Стандартный формат данных для джойстика. Получая данные от джойстика, программа помещает их в структуру DIJOYSTATE c_dfDUoystick2 Расширенный формат данных для джойстика. Получая данные от джойстика, программа помещает их в структуру DIJOYSTATE2, содержащую множество дополнительных элементов. Эти элементы позволяют игре поддерживать усовершенствованные джойстики, рулевые колеса и так далее После того, как метод SetupInputDevices () задаст формат данных для клавиатуры в строке 7 листинга 14.18, он задает размер буфера для буферизованного ввода (в строках 9-21). Если ваша игра будет использовать небуферизованный ввод, то эту часть метода в ней можно пропустить. В строках 28-40 листинга 14.18 видно, как задается размер буфера для буферизованного ввода с мыши. Подсказка Инициализация джойстиков и других устройств ввода выполняется практически по такой же схеме, что и показанная в методах initlnputDevices () и SetupInputDevices (). Основное отличие для этих устройств - то, что программа должна перечислить их кнопки, ползунки и оси перемещения. Кроме того, платформа предоставляет вам функцию, позволяющую опрашивать устройства ввода. Давайте посмотрим, как это делается.
406 Глава 14 Получение данных от клавиатуры и мыши Если ванта программа использует небуферизованныи ввод, она должна вызывать метод IDirectInputDevice8: : GetDeviceState (). Если используется буферизованный ввод, нужно вызывать метод IDirectlnputDeviceS: :GetDeviceData (). В листинге 14.19 показана работа с буферизованным вводом. Листинг 14.19. Метод process_input() 1 bool my_game::Processlnput() 2 { 3 DIDEVICEOBJECTDATA keyboardData[KEYBOARD_BUFFER_SIZE]; 4 DIDEVICEOBJECTDATA mouseData[MOUSE_BUFFER_SIZE]; 5 HRESOLT result; 6 DWORD totalElements; 7 DWORD i; 8 9 totalElements = KEYBOARD_BUFFER_SIZE; 10 result = theApp.Keyboard()->GetDeviceData( 11 sizeof(DIDEVICEOBJECTDATA), 12 keyboardData, 13 fitotalElements, 14 0) ; 15 16 if (result != DI_OK) 17 { 18 result = theApp.Keyboard()->Acquire() ; 19 while (result == DIERR_INPUTLOST) 20 { 21 result ss theApp.Keyboard()->Acquire() ; 22 } 23 24 /* Если приоритет принадлежит другому приложению или 25 произошла еще какая-то ошибка... */ 26 if ((result = DIERR_OTHERAPPHASPRIO) || 27 (result = DIERR_N0TACQOIRED)) 28 { 29 // Просто попробуем еще раз позже. 30 return (true); 31 ) 32 } 33 else 34 { 35 bool cameraMoved = false; 36 37 for (i=0; KtotalElements; i++) 38 {
Готовимся создавать игры 407 39 switch <keyboardData[i].dwOfs) 40 { 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 } 65 } 66 67 totalElements = MOUSE_BUFFER_SIZE; 68 result = theApp.Mouse()->GetDeviceData( 69 sizeof(DIDEVICEOBJECTDATA), 70 mouseData, 71 StotalElements, 72 0) ; 73 74 /* С помощью мыши в программе не выполняются никакие действия. 75 Код приведен здесь в демонстрационных целях.*/ 76 if (result != DI_0K) 77 { 78 result = theApp.Mouse()->Acquire(); 79 while (result = DIERR_INPUTLOST) 80 { 81 result = theApp.Mouse()->Acquire(); 82 } 83 84 /* Если приоритет принадлежит другому приложению или 85 произошла еще какая-то ошибка... */ 86 if ((result == DIERR_OTHERAPPHASPRIO) || 87 (result == DIERR_NOTACQUIRED)) // Стрелка вверх. case DIK_OP: // Вставьте сюда код, который должен выполняться //при нажатии стрелки вверх. break; // Стрелка вниз. case DIK_DOWN: // Вставьте сюда код, который должен выполняться //при нажатии стрелки вниз. break; // Стрелка влево. case DIK_LEFT: // Вставьте сюда код, который должен выполняться //при нажатии стрелки влево. break; // Стрелка вправо. case DIK_RIGHT: // Вставьте сюда код, который должен выполняться //при нажатии стрелки вправо. break;
408 Глава 14 88 { 89 // Просто попробуем еще раз позже. 90 return (true); 91 } 92 } 93 94 return (true); 95 } Метод, приведенный в листинге 14.19 - это еще один из методов класса игры. Прежде чем он вызовет метод UpdateFrame (), платформа вызывает метод Processlnput (). Это позволяет игре реагировать на действия пользователя, прежде чем обновлять кадр соответственно смоделированной физике. Каждому буферизованному устройству ввода требуется отдельный буфер. Метод Processlnput () объявляет буфера ввода для клавиатуры и мыши в строках 3-4 листинга 14.19. Размеры этих буферов должны быть как минимум равны размерам, которые игра передает функции SetProperty () из Directlnput. Вспомните, что функция SetProperty () вызывалась в листинге 14.18. Массивы, объявленные в строках 3-4 листинга 14.19 - это массивы структур DIDEVICEOBJECTDATA. Чтобы извлечь данные из буфера клавиатуры, из метода Processlnput () вызывается метод IDirectInputDevice8: :GetDeviceDa- ta() в строках 10-14. Первый параметр метода GetDeviceData () - это количество байтов в структуре DIDEVICEOBJECTDATA. Второй параметр - это адрес начала буфера клавиатуры, созданного в строке 3 листинга. Третий параметр метода GetDeviceData () — это общее количество структур в буфере. Когда метод GetDeviceData () заканчивает выполняться, в третьем параметре содержится действительное количество событий ввода, записанных в буфере. Последний параметр - это набор флагов. Единственный флаг, распознаваемый в данный момент - это DIGDD_PEEK, который позволяет вашей программе искать данные в буфере, не удаляя их. Последующие вызовы GetDeviceData () в этом случае будут возвращать те же данные. Обычно игры сбрасывают этот параметр в 0, и данные удаляются из буфера при их считывании. Если метод GetDeviceData () не может прочитать данные с клавиатуры, это обычно означает, что к ней потерян доступ. Это событие похоже на потерю графических поверхностей, но, к счастью, восстановить доступ к устройству ввода проще, чем восстановить графические поверхности. Все, что нужно для этого - вызвать метод IDirectInputDevice8: : Acquire (), как показано в строке 18 листинга 14.19. Возможно, метод IDirectInputDevice8: : Acquire () не сможет восстановить доступ к устройству ввода. Придется немного подождать. В этом случае в строке 19 метод Processlnput () начинает выполнять цикл while, ожидая, когда доступ будет восстановлен. Если доступ невоз-
Готовимся создавать игры 409 можно восстановить из-за какой-то другой ошибки, метод Processlnput () возвращает значение true и ожидает начала обработки следующего кадра, чтобы еще раз попытаться получить доступ к устройству ввода. Когда метод Processlnput () получает буферизованный ввод с клавиатуры, он начинает выполнять цикл for (строка 37 листинга 14.19). Цикл перебирает по очереди все события, которые метод GetDeviceDa- ta () передал методу Processlnput (). Идентификатор каждой нажатой клавиши содержится в структуре DIDEVICEOBJECTDATA в элементе dwOfs. В примере из листинга 14.19 обрабатываются нажатия на клавиши перемещения курсора (стрелки вверх, вниз, влево и вправо). Листинг 14.20. Восстановление доступа к клавиатуре и мыши после переключения между программами 1 bool my_game::HandleMessage( 2 HWND hWnd, г UIWI msg, 4 WPARAM wParam, 5 LPARAM lParam) 6 { 7 switch (msg) 8 { 9 case WM_ACTIVATE: 10 // Если мы получаем фокус..■ 11 if (WA_INACTIVE != wParam) 12 { 13 // Если клавиатура была настроена... 14 i f (theApp.Keyboard()) 15 { 16 // Удостоверяемся, «то доступ восстановлен 17 theApp.Keyboard()->Acqui re(); 18 } 19 20 // Если мышь была настроена... 21 if (theApp.Mouse()) 22 ( 23 // Удостоверяемся, «то доступ восстановлен 2 4 theApp.Mouse()->Acquire(); 25 } 26 } 27 break; 28 } 29 30 return (false); 31 } Буферизованный ввод от мышей и джойстиков обрабатывается почти так же, как и от клавиатуры.
410 Глава 14 Подсказка Идентификаторы, которые программа получает во входном буфере, различаются для клавиатур, мышей и джойстиков. Список идентификаторов клавиатуры можно найти в документации по DirectX в разделе «Keyboard Device Enumerated Type». Список идентификаторов мыши находится в разделе «Mouse Device Enumerated Туре», а идентификаторов джойстика - в разделе «Joystick Device Constants». Единственное, на что еще стоит обратить внимание - то, что устройства ввода часто теряются, если игрок нажимает Alt+Tab для переключения в другую программу. Вы можете обнаружить, что игра выполняется эффективнее, если она пытается вновь получить доступ к устройствам ввода каждый раз, когда игрок опять переключается на нее. В листинге 14.20 приведена версия метода HandleMessage () класса игры, который восстанавливает доступ к клавиатуре и мыши при реактивации игры. Завершение работы с Directlnput Прежде, чем завершить выполнение программы игры, нужно освободить все созданные в ней интерфейсы Directlnput. Это делается в методе Ga- meCleanup (). Вам не придется освобождать интерфейсы клавиатуры и мыши — платформа делает это автоматически. Перемещение камеры в DirectX Последняя тема, которую нужно рассмотреть, прежде чем мы приступим к написанию похожих на игры программ, - это перемещение камеры, то есть точки, из которой игрок видит ЗО-сцену. В DirectX оно реализуется довольно просто. Собственно говоря, вы уже знаете, как это делается. Взгляните на листинг 14.21. Листинг 14.21. Перемещение камеры в ответ на нажатия кнопок 1 bool my_game::Processlnput() 2 { 3 DIDEVICEOBJECTDATA keyboardData[KEYBOARD_BDFFER_SIZE]; 4 DIDEVICEOBJECTDATA mouseData[MOUSE_BUFFER_SIZE]; 5 HRESULT result; 6 DWORD totalElements; 7 DWORD i; 8 9 totalElements = KEYBOARD_BUFFER_SIZE; 10 result = theApp.Keyboard()-X3etDeviceData( 11 sizeof(DIDEVICEOBJECTDATA),
Готовимся создавать игры 411 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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 keyboardData, StotalElements, 0); if (result != DI OK) { result = theApp.Keyboard()->Acquire(); while (result = DIERR INPUTLOST) { > /* result = theApp.Keyboard()->Acquire(); Если приоритет принадлежит другому приложению или произошла еще какая-то ошибка... */ if < } } else { ((result = DIERR_OTHERAPPHASPRIO) || (result = DIERR_NOTACQUIRED)) // Просто попробуем еще pas позже. return (true); bool cameraMoved = false; for { ' (i=0; i<totalElements; i++) switch (keyboardData[i].dwOfs) { // Стрелка вверх. case DIK_UP: // Если клавиша была нажата... if (DirectlnputKeyDown(keyboardData[i].dwData)) { // Перемещаем камеру вперед. lookatPoint.z += 1.0; eyePoint.z += 1.0; cameraMoved = true; > break; // Стрелка вниз. case DIK_DOWN: // Если клавиша была нажата... if (DirectlnputKeyDown(keyboardData[i].dwData)) { // Перемещаем камеру назад. lookatPoint.z -= 1.0; eyePoint.z -= 1.0;
412 Глава 14 61 cameraMoved = true; 62 } 63 break; 64 65 // Стрелка влево. 66 case DIK_LEFT: 67 // Бели клавиша была нажата... 68 if (DirectInputKeyDown(keyboardData[i].dwData)) 69 { 70 // Перемещаем камеру влево. 71 lookatPoint.х -= 1.0; 72 eyePoint.x -= 1.0; 73 cameraMoved = true; 74 } 75 break; 76 77 // Стрелка вправо. 78 case DIK_RIGHT: 79 // Если клавиша была нажата... 80 if <DirectInputKeyDown(keyboardData[i].dwData)) 81 { 82 // Перемещаем камеру вправо. 83 lookatPoint.x += 1.0; 84 eyePoint.x += 1.0; 85 cameraMoved = true; 86 } 87 break; 88 } 89 } 90 91 // Если камера переместилась... 92 if (cameraMoved) 93 { 94 // Обновляем матрицу отображения. 95 D3DXMATRIX tempViewMatrix; 96 D3DXMatrixLookAtLH( 97 StempViewMatrix,SeyePoint,&lookatPoint,SupDirection); 98 theApp.ViewMatrix(tempViewMatrix); 99 ) 100 ) 101 102 totalElements = MOUSE_B0FFER_SIZE; 103 result = theApp.Моиse()->GetDeviceData( 104 sizeof(DIDEVICEOBJECTDATA), 105 mouseData, 106 &totalElements, 107 0) ; 108 /* С помощью мыши в программе не выполняются никакие 109 действия. Код приведен здесь исключительно в
Готовимся создавать игры 413 110 демонстрационных целях. */ 111 if (result != DI_OK) 112 { 113 result = theApp.Mouse()->Acquire(); 114 while (result = DIERR_INPUTLOST) 115 { 116 result = theApp.Mouse ()->Acquire () ; 117 } 118 119 /* Если приоритет принадлежит другому приложению 120 или произошла еще какая-то ошибка...*/ 121 if ((result = DIERRJDTHERAPPHASPRIO) || 122 (result == DIERR_NOTACQUIRED)) 123 { 124 // Просто попробуем еще раз позже. 125 return (true); 126 } 127 } 128 129 return (true); 130 } В листинге 14.21 приведен полный текст метода ProcessInputO класса игры. В этой версии изменяются положения точки, на которую направлена камера, и точки, в которой эта камера находится. Как это делается, видно в строках 47-49, 59-61, 71-73 и 73-85. Переменные loo- kAtPoint, eyePoint и cameraMoved - это private-элементы класса игры, как видно из листинга 14.22. Листинг 14.22. Класс игры с подвижной камерой 1 class my_game : public game 2 { 3 private: 4 // Геометрия и окружающая обстановка. 5 D3DXVECTOR3 eyePoint; 6 D3DXVECTOR3 lookatPoint; 7 D3DXVECTOR3 upDirection; 8 9 ground theGround; 10 11 public: 12 // Методы, используемые платформой. 13 bool OnAppLoad(); 14 bool PreD3DInitialization(); 15 bool PostD3DInitialization(); 16 bool Gamelnitialization(); 17 bool HandleMessage(
414 Глава 14 18 HWND hWnd, 19 UINT msg, 20 WPARAM wParam, 21 LPAEAM lParam); 22 bool ProcessInput(); 23 bool InvalidateDeviceObjects(); 24 bool RestoreDeviceObjectsО; 25 bool UpdateFrame(); 26 bool RenderFrame(); 27 bool GameCleanup(); 28 29 // Методы, используемые при инициализации. 30 void SetupInputDevices(); 31 void SetupViewMatrix(); 32 void SetupProjectionMatrix(); 33 void SetupGeometry(); 34 }; Поскольку переменные, управляющие положением камеры, теперь включены в класс игры, игра может изменять положение камеры в любой момент. Именно это и делается в методе Process Input () из листинга 14.21. Если пользователь нажимает клавиши перемещения курсора (стрелки вверх, вниз, влево или вправо), метод ProcessInput() изменяет значения переменных, отслеживающих положение камеры. В строках 92-99 листинга 14.21 метод Processlnput () передает Direct3D новое положение камеры. Он вызывает функцию D3DXMatrixLookAtLH (), чтобы создать новую матрицу отображения, и передает созданную матрицу классу d3d_app. Когда выполняется следующее обновление кадра, платформа автоматически обнаруживает, что матрица отображения изменилась, и передает Direct3D новую матрицу. Это делается точно так же, как и передача начальных матриц отображения в программах примеров. Если вы захотите посмотреть на код программы с подвижной камерой, то сможете найти его на прилагающемся к книге компакт-диске (в папке Source\Chapterl4\Camera). Итоги В этой главе мы кардинально переработали платформу физического моделирования. Эти изменения было необходимо сделать, чтобы платформу можно было использовать для создания реалистичных игр. На прилагающемся к книге компакт-диске обновленная платформа находится в папке Source\Chapter14\Framework.
Глава 15 Автомобили, корабли и лодки Пора применить изученную нами физику для создания игровых программ, моделирующих поведение реальных объектов. В этой главе мы рассмотрим автомобили, транспортные средства на воздушной подушке, корабли и лодки. Хотя такой набор может показаться произвольным, все эти объекты похожи с точки зрения их реализации в играх. Автомобили На первый взгляд в моделировании движущегося по дороге автомобиля нет ничего сложного. В предельно упрощенном варианте это и в самом деле несложно. Но если нужно добиться реалистичного поведения автомобиля, физика, которую придется моделировать, будет весьма сложна. Мощность, сила, ускорение и трение По историческим причинам мы обычно измеряем выходную мощность автомобильных двигателей в лошадиных силах (horse power — hp). Одна лошадиная сила - это примерно 0.75 киловатта. Для игр эти сведения не слишком полезны. Гораздо удобнее забыть о мощности и учитывать силу. Мощность и сила - это разные вещи. Мощность определяет общее количество работы, которую выполняет сила или вращающий момент за единицу времени. Если мы будем работать непосредственно с силами и моментами, а не с мощностями, мы упростим нашу задачу. Поршни в цилиндрах двигателей вращают коленчатый вал, который, в свою очередь, вращает карданный вал. Другими словами, двигатель развивает некоторое усилие, которое через цепочку передач прикладывается к ведущей оси (или осям, если у автомобиля больше двух ведущих колес). Разумеется, на каждом колесе есть шина. При вращении колеса вращающий момент шины прикладывает усилие к дороге. Это иллюстрирует рисунок 15.1.
416 Глава 15 Рис. 15.1. Силы, действующие на ведущее колесо Собственно говоря, вы уже видели этот рисунок раньше. Это повторение рисунка 13.7 из главы 13 «Вода и волны». Как видно из рисунка, шина прикладывает усилие к дороге, и дорога прикладывает направленное в обратную сторону усилие к шине. Это третий закон Ньютона. Всякое действие рождает равное противодействие. Сила взаимодействия между шиной и дорогой определяется трением. Как уже говорилось в главе 13, шина, которая не скользит по дороге, испытывает трение качения. Если усилие, прикладываемое к ведущему колесу, недостаточно для преодоления трения качения, колесо будет прикладывать к дороге силу, и сила, противодействующая этой силе, будет толкать машину вперед. Величина этой силы определяется вращающим моментом колеса. Ее можно найти по формуле: Г Если, например, колесо попадает на замерзшую лужу на дороге, коэффициент трения качения уменьшается, и сила, прикладываемая к дороге, может оказаться больше, чем сила трения качения. В этом случае шина начнет проскальзывать, и в действие вступит трение скольжения. При этом колесо будет вращаться практически на месте. Автомобиль будет почти неподвижен, а поверхность шины будет скользить по льду. Поскольку коэффициент трения скольжения меньше, чем трения качения, сила, которую скользящая шина приложит к дороге, меньше, чем сила, прикладываемая катящейся шиной, поэтому колесо будет вращаться, но автомобиль будет двигаться очень медленно. Вспомните - формула для расчета трения качения выглядит так: FS<^SN
Автомобили, корабли и лодки 417 Здесь Fs - это сила трения качения, /иа - коэффициент трения качения, а N - нормальная сила. В случае автомобиля нормальная сила - это сила тяжести F = mg. Поэтому мы можем записать: Fs < [ismg Это уже формула, которую можно использовать в программе. Если ваша игра моделирует движение автомобиля, то можно воспользоваться методами для опроса клавиатуры и джойстика, описанными в главе 14 «Готовимся создавать игры». Если пользователь нажимает стрелку вверх или наклоняет вперед джойстик, программа увеличивает усилие на ведущих колесах. Если это усилие превышает ^smg, колесо должно начать проскальзывать. При этом трение между шиной и землей должно вычисляться по формуле: FD<//DN Вернемся к ведущим колесам. Даже если вы считаете в игре, что у автомобиля единственное ведущее колесо, то силу, приложенную к нему, можно прикладывать к центру масс автомобиля. Это позволит рассматривать автомобиль как материальную точку с точки зрения поступательного движения. Но если вы хотите получить более или менее реалистичную модель автомобиля, вам придется моделировать приложение сил в точках, в которых ведущие колеса соприкасаются с землей. Это позволит моделировать, например, поведение автомобиля, скользящего по льду. Даже на самом скользком льду ведущее колесо прикладывает усилие к поверхности в точке соприкосновения с этой поверхностью. Поскольку эта точка не является центром масс автомобиля, то появляется вращающий момент, и автомобиль начинает разворачиваться. Если вы живете в стране, где нередки зимние морозы, то вы, вероятно, убеждались в этом на собственном опыте. Это не самый приятный опыт. Даже при езде по сухой и чистой дороге вращающий момент все равно присутствует. Однако он вызывает значительно меньшую силу, чем трение качения, поэтому машина двигается вперед, а не разворачивается. При езде по льду трение качения уменьшается, и момент может оказаться больше, чем сила трения, поэтому автомобиль начинает разворачиваться. Предупреждение Делать игры настолько реалистичными необязательно. В подавляющем большинстве случаев в этом нет необходимости. Следует выдерживать баланс между реализмом и быстродействием. Не забывайте, что вам нужно обеспечивать частоту обновления кадров - не меньше 30 кадров в секунду. Если вы хотите моделировать в игре заносы, программа должна отслеживать положение точек соприкосновения колес с грунтом. Есть несколько способов сделать это. Первый способ - определить вектор, указывающий на точку соприкосновения. Программа должна будет вращать и перемещать вектор по мере движения автомобиля.
418 Глава 15 В более сложных программах, использующих Direct3D, можно использовать другой способ. Программа вращает и перемещает сетчатую модель автомобиля. Проще говоря, она хранит результаты поворотов и перемещений в вертексном буфере. Если программе нужно найти точку соприкосновения колеса с грунтом, она просто считывает ее из вер- тексного буфера. Если выключить двигатель автомобиля и перевести передачу на нейтраль, то движущийся автомобиль постепенно остановится (если только он не катится вниз по склону). Причин остановки две. Первая - сопротивление воздуха (мы рассмотрим его ниже). Вторая - трение между колесами автомобиля и другими его компонентами. Если нам нужно точно моделировать автомобиль, мы должны моделировать и это трение. Однако чаще всего это слишком сложная вычислительная задача для игр. Так как же смоделировать трение движущихся частей автомобиля, не просчитывая трения? Нужно имитировать трение. Создавая класс автомобиля в программе, добавьте в него коэффициент трения, который мы будем использовать для этой имитации. Обозначим его JUCF. Этот коэффициент будет участвовать в следующей формуле: vF= A - fiCF)\c Здесь vF есть скорость в конце заданного временного интервала, a vc - скорость в начале этого интервала. Значение/uCF должно лежать в пределах от 0.0 до 1.0. Согласно этой формуле скорость автомобиля будет уменьшаться с каждым кадром. Хотя эта формула и не выведена из физических законов, результат ее использования будет вполне реалистичным. Сопротивление воздуха движущимся автомобилям Сопротивление воздуха в играх моделируется несколькими способами. Если вы хотите, чтобы моделирование было точным с точки зрения физики, нужно находить силу сопротивления воздуха по формулам для вязкого трения из главы 13. Для удобства эти формулы приведены ниже в таблице 15.1. Таблица 15.1. Вычисление силы сопротивления воздуха Формула Описание _ Эту формулу можно использовать, если автомобиль * FD — — *-'FDv движется недостаточно быстро, чтобы вызвать турбулентность воздуха _ g Эту формулу нужно использовать при моделировании ''FD — —*-ТВу быстро движущихся автомобилей, например, гоночных
Автомобили, корабли и лодки 419 Сила FFD направлена в сторону, противоположную направлению движения автомобиля. Игра должна прибавлять силу FFD к силе, развиваемой ведущими колесами. Для упрощенного моделирования автомобилей можно учитывать сопротивление воздуха, не вычисляя обусловленной им силы. Это делается так же, как и моделирование трения движущихся частей автомобиля. Просто добавьте в класс автомобиля еще один коэффициент трения, который будет использоваться в следующей формуле: vF = A - ^AF)vc В этой формуле ju^p - коэффициент трения, связанного с сопротивлением воздуха. Как и значение jUqp, его значение должно лежать в пределах от 0.0 до 1.0. Предупреждение Не используйте такое упрощение при моделировании быстро движущихся автомобилей. Если вы пишете симулятор автомобильных гонок, вам придется использовать формулы из таблицы 15.1. Торможение Торможение автомобиля - это обратная сторона разгона. На автомобиль в обоих случаях действуют одни и те же силы. Тормоза замедляют вращение колес. Это приводит к появлению силы, противодействующей силе, прикладываемой колесами к грунту (см. рис. 15.2). Рис. 15.2. Силы, действующие на колесо при торможении Пока сила торможения, которую колесо прикладывает к дороге, меньше силы трения качения, автомобиль будет плавно замедлять движение и
420 Глава 15 постепенно остановится. Но если колеса окажутся заблокированными, они начнут скользить по дороге, и на смену трению качения придет трение скольжения. При этом дистанция торможения увеличится. Дистанцию торможения можно вычислить несколькими способами. Можно применить для этого следующую формулу: d = v2/Bg(jucos(Q) + sin@))) В этой формуле d - это дистанция торможения, v - скорость автомобиля в момент начала торможения, g - ускорение силы тяжести, /л - коэффициент трения (качения или скольжения, в зависимости от того, проскальзывают ли колеса автомобиля по грунту). Угол 0 присутствует в формуле на случай, если автомобиль движется вверх или вниз по склону - это угол наклона этого склона относительно горизонтали. Если вы правильно напишете игру, то вам не придется пользоваться этой формулой. Просто приложите к автомобилю силу торможения, и автомобиль постепенно остановится, пройдя соответствующее расстояние. Единственное, за чем нужно следить - идет ли торможение за счет трения качения или скольжения. Разумеется, это верно и для ускоряющегося автомобиля. Собственно говоря, и в случае ускорения, и в случае торможения действуют одни и те же силы, просто направление их действия меняется на противоположное. Замечание В отличие от сил, прикладываемых к дороге ведущими колесами, силы торможения всегда приложены к центру масс автомобиля, поскольку тормозами оснащены все четыре колеса. Все колеса равноудалены от центра масс, и сумма их векторов смещения будет равна О.1 Повороты автомобилей Моделирование поворотов автомобиля может быть как очень простой, так и чрезвычайно сложной задачей. В простейших имитаторах автомобили двигаются в одной плоскости. В этом случае поворот автомобиля сводится к простому изменению направления его движения. Это не физическая задача, но такая методика подходит для многих игр. В более реалистичных играх должны моделироваться силы, воздействующие на автомобиль при повороте. Некоторые из этих сил показаны на рисунке 15.3. 1 Это утверждение справедливо не всегда - если точка приложения силы торможения не совпадет с центром масс, то возникнет переворачивающий момент, и автомобиль может опрокинуться. Это бывает довольно часто, особенно если автомобиль перегружен. - (Прим. перев.).
Автомобили, корабли и лодки 421 ^Поворота Рис. 15.3. Силы, действующие на поворачивающий автомобиль Во время поворота на автомобиль, изображенный на рисунке 15.3, действует несколько сил. Во-первых, передние шины автомобиля прикладывают к нему поворачивающую силу. Если эта сила меньше, чем сила трения качения, автомобиль плавно повернет, в противном случае его занесет. Поворачивающая сила, прикладываемая колесами, - это центростремительная сила. Центростремительные силы мы рассматривали ранее в главе 9 «Динамика твердых тел». Центростремительная сила, прикладываемая колесами, разворачивает переднюю часть автомобиля. Эта центростремительная сила - на самом деле две силы, по одной на каждое из передних колес. Игра может приложить половину общей центростремительной силы к точке, в которой с грунтом соприкасается каждое из передних колес. Это самый точный способ моделирования. Можно приложить всю силу к точке между передними колесами. И тот, и другой метод будут работать, но второй метод не позволит вам смоделировать проколы шин. Замечание Центростремительная поворачивающая сила появляется вследствие трения качения между шинами и поверхностью грунта. Все скоростные дороги наклонены в местах поворотов, чтобы автомобили кренились, проходя повороты на больших скоростях. На рисунке 15.4 показаны силы, действующие на кренящийся автомобиль. Наклоненная дорога приложит к автомобилю дополнительную центростремительную силу, которая будет помогать автомобилю повернуть. Это особенно важно, если автомобиль движется быстро. Если дорога не будет наклонена, трения качения между шинами и ее поверхностью может не хватить, чтобы удержать автомобиль на дороге при повороте, и автомобиль слетит с дороги. Наклон помогает удержать автомобиль на дороге.
422 Глава 15 Рис. 15.4. Автомобиль на наклоненной дороге Замечание Когда вы находитесь в поворачивающем автомобиле, вы ощущаете центробежную силу, толкающую вас в направлении от центра поворота. Эта сила обусловлена инерцией вашего тела. Ваше тело стремится двигаться в том же направлении, в котором оно двигалось до начала поворота. Это первый закон Ньютона - движущиеся объекты стремятся двигаться в том же направлении и с той же скоростью, что и раньше. К ним необходимо приложить силу, чтобы заставить их изменить скорость или направление движения. Из-за чего на наклонных поворотах появляется центростремительная сила? Из-за силы тяжести. Посмотрите на рисунок 15.4. Сила тяжести тянет автомобиль вниз. Поскольку дорога наклонена, нормальная сила тоже наклонена. У нормальной силы есть составляющая, толкающая автомобиль вверх, противодействуя силе тяжести. Кроме того, у нормальной силы есть составляющая, толкающая автомобиль вниз по уклону дороги. Поэтому, чтобы смоделировать поворачивающие силы, действующие на автомобиль, можно воспользоваться такой формулой: ^Поворота - FCT + FCR центростремите- = fiN + NH Здесь FnoBOpoTa есть общая поворачивающая сила, FCT - льная сила, обусловленная трением колес о поверхность дороги, FCR - центростремительная сила, обусловленная наклоном дороги. Сила FCT равна ,uNcos0, где pL - коэффициент трения (качения или скольжения, в зависимости от того, скользит ли машина по поверхности), N - нормальная сила, равная mg. В предыдущей формуле NH - горизонтальная составляющая нормальной силы, направленная к центру поворота. Можно найти предельную скорость, при превышении которой на повороте машина уйдет в занос. Это можно сделать по формуле: vT=Vrg(tane+L0
Автомобили, корабли и лодки 423 В этой формуле g - ускорение силы тяжести, г — радиус кривизны поворота, а Э - угол наклона дороги, ц - коэффициент трения качения между шинами и дорогой. Значение vT - предельная тангенциальная скорость автомобиля, при превышении которой он уйдет в занос. Замечание В игре значение/г нужно уменьшать, если состояние дороги будет неидеальным (например, мокрая или обледенелая дорога). Если скорость автомобиля в игре превысит vT, игра должна смоделировать занос автомобиля и заменить трение качения трением скольжения. Реализация простой модели автомобиля В своем простейшем варианте автомобиль в игре - это просто специальная разновидность твердого тела. В листинге 15.1 приведено определение класса автомобиля. Листинг 15.1. Простейший класс автомобиля 1 class basic_car : public rigid_body 2 { 3 private: 4 vector_3d initialForwardDirection; 5 vector_3d initialUpDirection; 6 7 force driveWheelForce; 8 9 scalar turnAngle; 10 11 scalar dragCoefficient; 12 13 public: 14 basic_car(); 15 16 void InitialForwardDirection( 17 vector_3d forward); 18 vector_3d CurrentCarDirection(void); 19 20 void InitialUpDirection( 21 vector_3d up); 22 23 void DriveWheelForce( 24 force driveForce); 25 force DriveWheelForce(void); 26
424 Глава 15 27 void TurningAngle( 28 scalar theAngle); 29 scalar TurningAngle(void); 30 31 void DragCoefficient(scalar drag); 32 scalar DragCoefficient(void); 33 34 void WheelsLocked(bool areLocked); 35 bool WheelsLocked(); 36 37 bool Update( 38 scalar changelnTime); 39}; В классе автомобиля из листинга 15.1 объявляется пара векторов, отслеживающих ориентацию автомобиля. Если программа разворачивает автомобиль, она должна разворачивать и эти два вектора. Кроме того, в классе есть вектор, описывающий усилие, развиваемое ведущими колесами. Если это усилие положительно, значит, колеса толкают автомобиль вперед. Если оно отрицательно, то автомобиль тормозится - колеса толкают его назад. В строке 9 листинга 15.1 объявлен private-элемент данных turnAng- 1е. В этом элементе хранится угол поворота. Положительный угол поворачивает автомобиль влево, отрицательный - вправо. Заметьте, что это угол поворота относительно положительного направления оси у. Автомобиль поворачивается только в плоскости xz - в данной реализации он не может крениться. Последний private-элемент данных — это коэффициент трения для моделирования сопротивления воздуха. Методы класса basic_car в основном просто считывают и задают значения элементов данных. Но реализации двух методов стоит рассмотреть поподробнее. Эти методы приведены в листинге 15.2. Листинг 15.2. Базовые методы класса basic_car 1 vector_3d basic_car::CurrentCarDirection(void) 2 { 3 vector_3d currentDirection; 4 5 // Сначала создадим двумерную матрицу вращения. 6 matrix2x2 carRotationMatrix; 7 carRotationMatrix.Element( 8 0, 9 0, 10 cosf(rigid_body::CurrentOrientation().YAngle())); 11 if (CloseToZero ( 12 carRotationMatrix.Element@,0)))
Автомобили, корабли и лодки 425 13 < 14 carRotationMatrix.Element@,0,0.0); 15 } 16 17 carRotationMatrix.Element( 18 0, 19 1, 20 sinf(rigid_body::CurrentOrientation().YAngle())); 21 if (CloseToZero( 22 carRotationMatrix.Element@,1))) 23 { 24 carRotationMatrix.Element@,1,0.0); 25 } 26 27 carRotationMatrix.Element( 28 1, 29 0, 30 -sinf(rigid_body::CurrentOrientation().YAngle())); 31 if (CloseToZero( 32 carRotationMatrix.ElementA,0))) 33 { 34 carRotationMatrix.ElementA,0,0.0); 35 } 36 37 carRotationMatrix.Element( 38 1, 39 1, 40 cosf(rigid_body::CurrentOrientation().YAngle())); 41 if (CloseToZero( 42 carRotationMatrix.ElementA,1))) 43 { 44 carRotationMatrix.ElementA,1,0.0); 45 } 46 47 // Помещаем начальное направление в двумерный вектор. 48 vector_2d temp2DVector; 4 9 temp2DVector.X( 50 initialForwardDirection.X()); 51 temp2DVector.Y( 52 initialForwardDirection.Z()) ; 53 54 // Теперь умножаем вектор на матрицу вращения. 55 temp2DVector = 56 carRotationMatrix * temp2DVector; 57 58 // Создаем требуемый трехмерный вектор. 59 currentDirection.X( 60 temp2DVector.X()); 61 currentDirection.Y@.0) ;
426 Глава 15 62 currentDirection.Z( 63 temp2DVector.Y{)) ; 64 65 return (currentDirection); 66 } 67 68 bool basic_car::Update(scalar changelnTime) 69 { 70 71 /* Определяем направление движения автомобиля, находя 72 вектор, направленный из центра масс в точку посередине 73 между двумя передними колесами. */ 74 vector_3d carDirection; 75 carDirection = CurrentCarDirection(); 76 carDirection = carDirection.Normalize(SCALAR_TOLERANCE); 77 78 // 79 // Прикладываем к автомобилю все существующие 80 // разворачивающие силы. 81 force wheelTurningForce; 82 vector_3d turnDirection; 83 84 // Если автомобиль движется и поворачивается... 85 if ((!CloseToZero(point_mass::LinearVelocity().Norm())) &S 86 (!CloseToZero(turnAngle))) 87 { 88 // Преобразуем в радианы. 89 turnAngle = DegreesToRadians(turnAngle); 90 91 // Положительный угол поворота разворачивает влево. 92 // Отрицательный разворачивает вправо. 93 angle_set_3d changelnAngle; 94 changelnAngle.SetXYZ( 95 0.0, 96 rigid_body::CurrentOrientation().YAngle() + turnAngle, 97 0.0); 98 rigid_body::CurrentOrientation(changelnAngle); 99 } 100 101 // 102 // Прикладываем к автомобилю усилие от ведущих 103 // колес. 104 105 if (!CloseToZero(driveWheelForce.Force() .Z())) 106 { 107 /* Направляем усилие в направлении движения 108 автомобиля, если он ускоряется, или в 109 противоположном направлении, если тормозится.*/ 110 force engineForce;
Автомобили, корабли и лодки 427 111 engineForce.Force( 112 driveWheelForce.Force().Z() * carDirection); 113 engineForce.ApplicationPoint(vector_3d@.0,0.0,0.0)); 114 115 engineForce.Force( 116 engineForce.Force() + 117 point_mass::ImpulseForce().Force()); 118 engineForce.ApplicationPoint( 119 point_mass::ImpulseForce().ApplicationPoint()); 120 point_mass::ImpulseForce(engineForce); 121 ) 122 else 123 { 124 driveWheelForce.Force(vector_3d@.0,0.0,0.0)); 125 ) 126 127 // 128 // Прикладываем силу сопротивления воздуха 129 // 130 vector_3d windResistance; 131 windResistance = 132 point_mass::LinearVelocity() * dragCoefficient; 133 point_mass::LinearVelocity( 134 point_mass::LinearVelocity() - windResistance); 135 if (CloseToZero(point_mass::LinearVelocity().Norm())) 136 { 137 point_mass: .-LinearVelocity (vector_3d@. 0, 0. 0, 0. 0) ) ; 138 ) 139 140 rigid_body::Update(changeInTime); 141 142 return (true); 143 } Первый метод в листинге 15.2 называется CurrentCarDirection(). Этот метод вычисляет и возвращает направление, в котором ориентирован автомобиль в трехмерном пространстве. Он должен это сделать, поскольку в классе автомобиля сохраняется только ориентация автомобиля в момент начала расчета кадра. Если бы в классе сохранялась величина поворота, то автомобиль разворачивался бы на эту величину в каждом кадре. Вместо этого в классе хранятся начальная ориентация и угол поворота - смещение от начальной ориентации. И в каждом кадре программа вычисляет новую ориентацию с помощью метода CurrentCarDirection (). Чтобы найти текущую ориентацию автомобиля, метод CurrentCarDirection () создает двумерную матрицу вращения. Двумерной матрицы достаточно, поскольку наша модель автомобиля может поворачиваться и перемещаться только в одной плоскости - xz. Автомобиль не может крениться, но во многих играх это и не требуется.
428 Глава 15 Подсказка Если вы хотите, чтобы в вашей игре автомобили кренились, проходя повороты, вам придется переопределить методы CurrentCarDirection () и Update () в вашем производном классе автомобиля. Новые версии этих методов должны выполнять вращение в трехмерном пространстве, а не на плоскости. Завершив создание двумерной матрицы вращения, метод CurrentCarDirection () создает двумерный вектор (в строках 48-52). В этом векторе хранится начальная ориентация автомобиля. Заметьте, что, поскольку автомобиль двигается в плоскости xz, в компоненте х вектора хранится компонент х направления, а в компоненте у вектора - компонент z направления. В строках 55-56 листинга 15.2 метод CurrentCarDirection () умножает двумерный вектор на двумерную матрицу вращения. Результаты умножения он сохраняет в трехмерном векторе в строках 59-63. Метод завершается, возвращая этот трехмерный вектор. Далее в листинге 15.2 приведен код метода Update (). Первая задача, которую выполняет этот метод - вызывает метод CurrentCarDirection (), чтобы получить текущую ориентацию автомобиля. Затем он нормализует вектор. Вспомните, что сила, прикладываемая к автомобилю ведущими колесами, направлена всегда вдоль оси z. Если автомобиль тормозит, то сила прикладывается в обратном направлении. Это позволяет программе разгонять и тормозить моделируемый автомобиль так же, как обычный автомобиль. Однако это значит, что метод Update () должен направлять силу, прикладываемую к автомобилю ведущими колесами, в направлении движения автомобиля. Кроме того, метод Update () должен знать текущую ориентацию автомобиля, чтобы правильно его разворачивать. Чтобы выполнить эти операции, метод Update () сначала вычисляет ориентацию автомобиля. Если автомобиль движется и разворачивается, метод Update () поворачивает его на нужный угол в строках 93-99 листинга 15.2. Чтобы сделать это, он добавляет угол поворота к текущей ориентации. Поскольку крен автомобиля не моделируется, вращение выполняется только вокруг оси у. Метод Update () прикладывает к автомобилю усилие от ведущих колес в строке 110. Он направляет это усилие в направлении движения автомобиля в строках 111-113. Поскольку это простейшая модель автомобиля, в ней усилия не прикладываются в точках соприкосновения колес с землей. Вместо этого это усилие прикладывается к центру масс автомобиля. Этот прием работает достаточно хорошо во многих играх. Сила, прикладываемая к автомобилю ведущими колесами, - это импульсная сила. Она не является постоянно действующей, поскольку автомобиль может разгоняться или тормозиться с течением времени. Метод Update () сохраняет характеристики этой силы в строках 115-120. В методе Update () демонстрируется простой метод вычисления сопротивления воздуха в строках 130-134 листинга 15.2. В строке 140 метода
Автомобили, корабли и лодки 429 Update () вызывается метод rigid_body: : Update (), который прикладывает к автомобилю постоянно действующие и импульсные силы, перемещая его в трехмерном пространстве. В классе basic_car стоит обратить внимание на один существенный момент: он не принимает в расчет силу тяжести. Значит ли это, что автомобиль летит над землей? Конечно же, нет, поскольку сила тяжести есть внешняя сила. Она притягивает автомобиль к земле, но земля толкает его вверх с той же силой. Соответственно, сумма внешних сил, действующих на автомобиль, равна 0, и их можно не учитывать. Учитывать силу тяжести нужно только в тех случаях, когда автомобиль отрывается от земли - при этом сумма внешних сил перестает быть равной 0. Транспорт на воздушной подушке и антигравитационные транспортные средства Транспорт на воздушной подушке весьма похож на автомобили с точки зрения их моделирования в играх. С этой точки зрения корабли на воздушной подушке - это просто разновидность автомобилей с другими характеристиками поворотов. Из моделей транспортных средств на воздушной подушке удобно создавать модели футуристических машин - например, антигравитационных. Как работает воздушная подушка В кораблях на воздушной подушке используется «юбка», удерживающая воздух, выдуваемый вниз с помощью вентилятора. Этот воздух создает «подушку», на которой скользит сам корабль. На рисунке 15.5 показано, как это происходит. Корпус транспортного средства и прикрепленная к нему гибкая юбка образуют камеру, в которую вентилятор постоянно нагнетает воздух. При движении транспортного средства воздушная подушка позволяет ему пересекать неровности почвы и водные пространства. Юбка сделана из гибкого материала, поэтому при столкновениях с незначительными препятствиями, например, камнями или невысокими волнами на поверхности воды, она прогибается, оставаясь невредимой. Поскольку транспортные средства на воздушной подушке не соприкасаются с поверхностью, над которой двигаются, они практически не испытывают трения. Такие транспортные средства могут пройти гораздо большее расстояние, чем автомобиль, получив толчок той же энергии. Кроме того, они гораздо сильнее, чем автомобили, стремятся двигаться по прямой, и их труднее разворачивать.
430 Глава 15 t t * t * t t Камера Вентилятор жесткая рама i % * tii , t < • «it i » i » * * * i * * * Hit I t » \ I g I I % * * F * * . % * * * 4 : ч * * * * i ^ % * воздушная подушка гибкая юбка Рис. 15.5. Воздушная подушка Остановить движущееся транспортное средство на воздушной подушке можно двумя способами. Первый - позволить сопротивлению воздуха сделать это. Обычно в играх такой способ не годится - сопротивление воздуха слишком мало, чтобы остановить транспортное средство достаточно быстро. Второй способ - создать на транспортном средстве поток воздуха, направленный вперед. Этот поток приложит к транспортному средству ту же силу, которую к автомобилю прикладывают тормоза. Но учтите, что такие «тормоза» не будут работать так же резко, как обычные тормоза в автомобиле. Если вы хотите точно смоделировать их в игре, вы должны сделать торможение медленным и плавно нарастающим. Сопротивление воздуха транспортным средствам на воздушной подушке Сопротивление воздуха при моделировании транспортного средства на воздушной подушке моделируется точно так же, как и при моделировании автомобиля. Расчеты выполняются по тем же формулам, что и для погруженных в воду объектов в главе 13.
Автомобили, корабли и лодки 431 Повороты транспортных средств на воздушной подушке При поворотах транспортные средства на воздушной подушке ведут себя совсем не так, как автомобили. При повороте они не кренятся. Для выполнения поворота они используют один из двух механизмов: Q хвостовые рули (похожие на самолетные), чтобы развернуться в нужном направлении; □ реактивные двигатели или пропеллеры, закрепленные на носу, которые могут использоваться как при торможении, так и при поворотах. Если вы хотите смоделировать поворот с помощью хвостовых рулей, нужно моделировать давление воздуха на их плоскости. Это практически то же самое, что моделировать воздействие течения воздуха на эти плоскости. Моделирование течения воздуха выполняется так же, как и моделирование течения воды (см. раздел «Течения в воде» в главе 13). Усилия, которые течение воздуха прилагает к рулям, приводит к возникновению вращающего момента, который разворачивает транспортное средство. Поворот всегда выполняется вокруг вертикальной оси. Когда транспортное средство развернется на нужный курс, пилот разгоняет его на этом курсе с помощью маршевых двигателей. При использовании носовых двигателей тоже возникает вращающий момент, который приводит к повороту транспортного средства вокруг его центра масс. Использование этих двигателей делает поведение транспортных средств на воздушной подушке немного более похожим на поведение автомобилей. Корабли и лодки При написании игр реализация кораблей и лодок весьма похожа на реализацию сложных моделей автомобилей и транспортных средств на воздушной подушке. Да, есть определенные специфические задачи, но основные принципы остаются неизменными. Плавучесть кораблей и лодок В главе 13 мы подробно изучили причины плавучести предметов. Мы вывели формулу для вычисления выталкивающей силы: Fb = Р£у Здесь Fb - направленная вверх выталкивающая сила, р — плотность воды, V — объем погруженной в воду части тела, на которое действует эта выталкивающая сила (например, части лодки или корабля).
432 Глава 15 Замечание Никогда не спрашивайте моряков, на каких лодках они плавают. Они бросят на вас раздраженный взгляд и пояснят, что лодки - это то, на чем можно заниматься греблей на пруду, а моряки ходят на кораблях. Ну ладно. Но ведь нужно знать объем корпуса корабля, который очень сложно вычислить. Давайте разберемся, как сделать это в играх. Вычисление объема корпуса Было бы здорово, если бы существовал простой метод нахождения объема корпуса. Но, увы, такого метода нет. Форма корпусов кораблей и лодок может быть очень сложной. Их обводы представляют собой гладкие кривые, в корпусах могут быть выемки или выступы. Нахождение точного объема корпуса - сложная задача даже для инженеров-кораблестроителей. Значит, и нам придется возиться с замысловатыми формулами и математическим анализом, чтобы писать игры? Вы, вероятно, уже догадались, что я предложу какие-нибудь приемы, позволяющие приближенно вычислить объем корпуса с точностью, достаточной для игр. Далее в играх, в которых имитируются гонки на катерах, приближенные методы нахождения объемов отлично работают. Если вычисления будут давать несколько отличающийся от истины результат, это обычно не страшно. Почему? Потому что ошибка будет почти одинаковой для всех корпусов в игре, и соотношение объемов корпусов будет правильным. Проще всего представить корпус корабля с помощью параллелепипедов и пирамид. Да, пирамид. Как это делается, показано на рисунке 15.6. ^^"^ ^ ^ \ \ Рис. 15.6. Приближенное представление объема корпуса Да, сочетание из параллелепипеда и пирамиды на рисунке 15.6 не слишком напоминает корпус корабля. Его нижняя сторона не изогнута, как днище корабля, бока не выступают наружу, как борта, а нос не напоминает корабельный нос. Так как же это приближение работает? Ответ: достаточно хорошо для игр.
Автомобили, корабли и лодки 433 Параллелепипед - часто достаточно хорошее для игр приближенное представление корпуса корабля. Если вам нужно найти объем корпуса сложной формы, например, корпуса гидроплана, можно использовать не один, а несколько параллелепипедов. Это позволит получить достаточно точный результат. Подсказка Объем параллелепипеда равен длина х высота х ширина. Учесть остроконечный нос корабля немного сложнее. Он заострен, чтобы при движении вода оказывала меньшее сопротивление кораблю. Чем больше заострен нос, тем сложнее точно найти объем корпуса корабля. Проще всего приближенно найти объем носовой части, рассматривая ее как пирамиду. Объем такой пирамиды равен: 1/3 (длина х ширина). Здесь длина и ширина - это соответственно длина и ширина основания пирамиды. Для более точного приближения можно использовать четырехгранную пирамиду. Эта фигура называется тетраэдром и состоит из равносторонних треугольников. Равносторонний треугольник - это треугольник, длины всех сторон которого равны. Каждый угол такого треугольника всегда равен 60°. Вот формула для вычисления объема тетраэдра: 1/3 (площадь х высота). Здесь площадь - это площадь основания тетраэдра, а высота - высота тетраэдра. Площадь основания (треугольника) можно найти по формуле S = 1/2 A х h). Здесь S - площадь треугольника, 1 - длина его стороны, ah — его высота. Длина высоты в треугольнике, входящем в тетраэдр, равна: 2 Как видите, использовать тетраэдр немного сложнее, чем пирамиду, но это дает гораздо более точное приближение объема корпуса корабля. Остойчивость кораблей и лодок Корабли могут быть остойчивыми и неостойчивыми. Остойчивость определяется взаимным расположением центра массы, центра водозмещения (плавучести) и метацентра корабля. Они показаны на рисунке 15.7.
434 Глава 15 Выравнивающий момент Центр I водоизмещения / / Рис. 15.7. Остойчивость корабля На рисунке 15.7 центр массы корабля расположен довольно низко в его корпусе. Это позволяет сделать корабль остойчивым. Центр водоизмещения - это точка, к которой приложена выталкивающая сила. Если корабль стоит на ровном киле (без крена), то центр водоизмещения должен находиться ниже центра массы. Если корабль накренится на левый борт или на правый борт, центр водоизмещения сместится в направлении крена. Направление действия выталкивающей силы всегда остается неизменным. Точка пересечения прямой, по которой прикладывается выталкивающая сила, и плоскости симметрии корабля, называется метацентром корабля . Чтобы корабль был остойчивым (не переворачивался при крене), метацентр должен находиться выше центра тяжести. Если корабль накренится слишком сильно, метацентр окажется ниже центра тяжести. Если это случится, корабль перевернется. Замечание В больших кораблях в нижней части корпуса размещают дополнительный груз - балласт. Он понижает точку центра массы и повышает остойчивость корабля, мешая ему опрокидываться. Когда корабль кренится, центры тяжести и водоизмещения смещаются друг относительно друга, поскольку центр водоизмещения смещается к одному из бортов. Сила тяжести всегда направлена вертикально вниз и приложена к центру масс, а выталкивающая сила - вертикально вверх, и приложена к центру водоизмещения. Если эти два центра не находятся на одной вертикальной прямой, то приложенные к ним силы приводят к появлению вращательного момента, который будет возвращать корабль в состояние равновесия. Этот момент называется выравнивающим моментом. 1 Этот раздел изложен с применением терминологии, принятой в отечественной литературе. - (Прим. перев).
Автомобили, корабли и лодки 435 Замечание Когда корабль возвращается ворят, что он выравнивается. в обычное положение, будучи накренен, го- Моделирование такого поведения в играх может быть очень сложным, если подойти к нему с требуемой тщательностью. Нахождение центра масс, центра водоизмещения и метацентра - это задача, которую лучше оставить профессиональным инженерам и кораблестроителям. Лучшее, что мы можем сделать в играх - в явном виде задать положение этих точек. Возможно, вам придется поэкспериментировать, чтобы найти подходящие параметры. Задав положение центра масс и метацентра, можно приступить к моделированию. Когда корабль начинает крениться, метацентр движется по дуге окружности, в центре которой находится центр тяжести. Выравнивающий момент можно найти, приложив выталкивающую силу к метацентру, а не к центру водоизмещения. С точки зрения физики неверно прикладывать ее к метацентру, но это избавляет нас от необходимости находить центр водоизмещения. Предупреждение Это объяснение подразумевает, что корабль не перегружен до такой степени, что центр массы будет выше метацентра. Если корабль настолько перегружен, то приложить выталкивающую силу к метацентру накренившегося корабля нельзя - в модели уже будет присутствовать вращающий момент, направленный в сторону крена корабля, поскольку метацентр будет ниже центра массы. Масса и виртуальная масса Масса движущегося корабля может в действительности быть больше, чем масса самого корабля и его груза. При движении корабля вследствие трения за ним устремляется окружающая корабль вода. Можете считать, что некоторый объем воды «прилипает» к корпусу корабля из-за трения. В результате частицы воды, контактирующие с корпусом, имеют ту же скорость, что и корпус. Чем дальше частица воды от корпуса, тем меньше ее скорость зависит от скорости движения корабля. На некоторой дистанции корпус корабля вообще не будет возмущать воду при движении. Масса корабля плюс масса увлекаемой им воды называется виртуальной массой. У больших кораблей слой увлекаемой ими воды может иметь толщину несколько метров. Он заметно увеличивает массу, которая используется в расчетах всех сил и сопротивлений. Чтобы точно моделировать корабли, игра должна учитывать виртуальную массу больших кораблей.
436 Глава 15 Замечание У маленьких кораблей и лодок виртуальная масса практически не отличается от их собственной массы. Вычислять виртуальную массу в играх не нужно, если в них не моделируются крупные корабли вроде танкеров или авианосцев. Точное вычисление виртуальной массы - это еще одна задача, решение которой мы оставим профессиональным кораблестроителям. Она слишком сложна, чтобы компьютер справился с ней за время просчета одного кадра игры. Но приближенно оценить виртуальную массу несложно. В большинстве методов приближенной оценки используются эллипсоиды той же длины, что и корпус корабля. Я опишу результат таких оценок, не погружаясь в их детали. Для оценки виртуальной массы нам нужен коэффициент, который можно умножить на массу, чтобы получить правдоподобный результат. Чтобы найти этот коэффициент, представьте себе, что у нас есть корабль, ширина которого равна 0. Такой корабль создать невозможно, но представьте себе, что он существует. При нулевой ширине добавочная масса будет равна 0. Методы оценки с помощью эллипсоидов показывают, что коэффициент виртуальной массы при ширине 0 равен 0.0. Теперь представьте себе, что у нас есть корабль, длина которого равна его ширине. Корабль будет иметь форму круга. Для такого корабля методы оценки с помощью эллипсоидов показывают, что коэффициент виртуальной массы равен 0.5. Используя эти предельные результаты, мы можем выразить виртуальную массу корабля как процент от его массы. Например, для сферического корабля виртуальная масса равна его собственной массе, умноженной на коэффициент A.0 + 0.5), т. е. 150 % его собственной массы. Я могу честно признаться, что никогда не видел сферического корабля. У большинства реально существующих кораблей добавочная масса не превышает 20 % их собственной массы. Поэтому у этих кораблей виртуальная масса составляет до 120 % их собственной массы. Обычно она лежит в пределах от 104 % до 115 % собственной массы. С помощью коэффициента виртуальной массы это можно выразить: mv = m(l + с^). В этом уравнении m — это собственная масса корабля, т^ - его виртуальная масса, a cvm - коэффициент виртуальной массы. Значение cvmдолжно лежать в пределах от 0.0 до 0.5. Эту простую формулу можно использовать для приближенной оценки виртуальной массы при движении корабля. Затем найденную виртуальную массу можно использовать при расчетах движения корабля. Подсказка Чтобы найти оптимальный коэффициент виртуальной массы для корабля, придется поэкспериментировать с его параметрами. Для начала попробуйте значение 0.1, а затем при необходимости увеличивайте или уменьшайте его.
Автомобили, корабли и лодки 437 Корабли и сопротивление движению В главе 13 вы видели, что вода оказывает сопротивление движению объектов в ней. Это вязкое трение можно вычислять по формулам, приведенным ранее в этой главе. В таблице 15.1 приведены формулы расчета сопротивления воздуха. И воду, и воздух можно рассматривать как жидкости и использовать для расчета их сопротивления одни и те же формулы. Единственное отличие в том, что коэффициент вязкого трения для воздуха будет гораздо меньше, чем для воды. Однако при движении корабля в воде вязкое трение - не единственный эффект, способный оказывать ему сопротивление. Будучи на корабле, пройдите на корму (задний конец корабля) и посмотрите на воду за ней. Не важно, находитесь ли вы на паруснике или на корабле с механическим приводом, вода за движущимся кораблем все равно будет завих- ряться. Это завихрение создает за кораблем зону пониженного давления. Разность давлений приводит к появлению силы, которая направлена против курса корабля. Это значит, что общее сопротивление выражается формулой: ■""общее ■"'трения ^ -"давления* Чтобы точно найти сопротивление, обусловленное разностью давлений, нужно хорошо знать гидродинамику и вычислительную математику. Но мы можем найти это сопротивление приблизительно. Оно никогда не превышает 10 % от сопротивления, вызванного трением. Поэтому можно изобрести формулу, выражающую сопротивление, обусловленное разностью давлений: общее ~~ "'трения^*■ + Удавления'' В этой формуле Кдавления - это сопротивление, обусловленное разностью давлений, а Ит^дня - сопротивление, обусловленное трением. Сдавления - это коэффициент, введенный специально для этой формулы, «коэффициент давления», значение которого изменяется от 0.0 до 0.10. В физике такой коэффициент не используется, но мы будем его применять для удобства моделирования. Этот коэффициент должен быть тем больше, чем больше корабль. Для кораблей с размерами меньше авианосцев и супертанкеров используйте значение меньше 0.05. Подсказка Если хотите моделировать поведение кораблей более детально, можете добавить сопротивление волн, толкающих корпус корабля. Но если ваш корабль не попал в шторм, не стоит этого делать. Если хотите смоделировать сопротивление волн, оценивайте его приближенно, как и сопротивление, обусловленное разностью давлений. Создайте «коэффициент волнового сопротивления» и используйте формулу йвопн = RTpeHMflA + Сволн), где Явопн - волновое сопротивление, а Сволн - коэффициент волнового сопротивления. Значение этого коэффициента может изменяться от 0.0 до 1.0 в зависимости от погодных условий.
438 Глава 15 Сопротивление воздуха Движущиеся с большой скоростью корабли испытывают значительное сопротивление воздуха, как и гоночные автомобили. Теперь вы уже знаете, как по формулам из таблицы 15.1 находить это сопротивление. Сопротивление воздуха может оказывать заметное влияние даже на движение больших медленно движущихся кораблей. Это происходит, если дует сильный ветер, что часто бывает на море. Если в вашей игре моделируются большие корабли в шторм, то сопротивление воздуха нужно учесть в расчетах. Но если скорость ветра меньше 60 км/ч, оно не окажет заметного влияния на движение больших кораблей. Течения и волны Чтобы достичь максимального реализма, игра может принять во внимание течения и волны. Разобраться с ними не так сложно, как вам, вероятно, кажется. Течения оказывают огромное влияние на движение кораблей и лодок. На рисунке 15.8 показано, почему это так. Требуемое Действительное положение положение А J_—+ _| I L. I \- -► г—+ г—-*- Течение J ► l ► I + I ► ! ► j_—_» \.—+ i _► Рис. 15.8. Течения в воде На рисунке показан вид сверху лодки, движущейся по воде. Течение направлено слева направо. Лодка должна двигаться снизу вверх, однако под действием течения она будет смещаться и вправо. Чтобы смоделировать течение в воде, представим, что рядом с нашей лодкой движется большая труба. В этой трубе течет вода, которая, выходя из трубы, толкает лодку. Представив себе это, мы можем использовать
Автомобили, корабли и лодки 439 для моделирования течений ту же формулу, которую мы использовали в главе 13 для моделирования движения воды в трубе. Вот эта формула: Fw = pAv2 Сечение нашей трубы - это половинка эллипса, длина которого равна длине корабля. Скорость - это скорость течения воды, а не скорость корабля. Замечание В программах сложные течения можно моделировать в виде соединенных последовательностей материальных точек. Но для подавляющего большинства игр достаточно метода «воображаемой трубы». Моделировать воздействие волн на корабли не так сложно, как моделировать сами волны. Волна - это, собственно говоря, течение, сила которого ритмично изменяется во времени. Чтобы найти силу, прикладываемую волной к кораблю, можно применить приведенную выше формулу. Но это не слишком реалистично. Сила, прикладываемая к кораблю волной, может толкать его вперед или назад, если она достаточно велика. Она может заставить корабль накрениться. В непогоду волны могут заставить нос корабля подниматься и опускаться. В шторм это раскачивание может стать настолько сильным, что корабль будет получать повреждения. Чтобы все это смоделировать, нужно использовать формулы для волн, приведенные в главе 13, в разделе «Волны». Вот эти формулы: F2T = F1T Г12/г22 F2L=F1Lri2/r22 Чтобы смоделировать качку, используйте первую формулу - она позволяет найти поперечную силу волны. Чтобы смоделировать толкающий эффект волн, используйте вторую формулу - по ней можно найти продольные силы, развиваемые волнами. Итоги Хотя в играх встречаются самые разные транспортные средства, методы, используемые для их моделирования, весьма схожи. Важно найти силы, которые к транспортному средству прикладывает окружающая среда, и силы, прикладываемые к окружающей среде этим транспортным средством. В подавляющем большинстве случаев этого достаточно, чтобы транспортное средство вело себя вполне реалистично.
Глава 16 Авиация и космические корабли Я поступил на свои первые курсы по компьютерной графике незадолго до вымирания динозавров. Ну, по крайней мере, иногда мне кажется, что это было так давно. Моим выпускным проектом была программа авиа- симулятора. Эта программа стала одной из самых сложных программ, которые мне пришлось написать. Почему? Частично потому, что я знал не слишком много о том, что собирался сделать. Позже я обнаружил, что большинство других студентов были куда менее амбициозны в своих проектах. Вторая причина заключалась в том, что мне нужно было писать программу с нуля на компьютере с EGA-видеоадаптером под управлением DOS. Если мне был нужен растеризатор, я должен был написать его сам. Я написал процедуры, выводившие на экран ландшафт, включая деревья и здания. В программе не было никаких «загрузок сетчатых моделей». А текстуры? Забудьте о текстурах. Ну ладно. С тех пор миновали два или три ледниковых периода. С помощью инструментов, которые у нас есть сейчас, написать простой авиа- симулятор можно в нескольких сотнях строк кода. В этой главе вы узнаете, как это сделать. Кроме того, мы разберемся с физическими основами функционирования самолетов, ракет и космических кораблей. Простой подход к авиасимуляторам Начиная обсуждение авиации и космических кораблей, мы рассмотрим несколько вариантов их имитации без использования физики. В этом нет ничего страшного. При написании игр такие методы использовались много лет, прежде чем появились авиасимуляторы, основанные на реалистичных физических моделях. Чтобы понять, как написать реалистичный авиасимулятор, нужно знать, как функционируют самолеты. На рисунке 16.1 показаны основные степени свободы самолета.
Авиация и космические корабли 441 7 \ 1 Тангаж | I / / / I I Крен / / / / Г Рыскание I I Рис. 16.1. Оси вращения самолета в воздухе Из рисунка 16.1 видно, что три основных перемещения самолета - это повороты. Поворот носа самолета вверх или вниз называется тангажом (поворот вниз приведет к пикированию, поворот вверх - к кабрированию). Поворот носа самолета влево или вправо называется рысканием. Поворот самолета вокруг продольной оси называется креном. Имитируя эти три основных поворота, можно изображать полет самолета.
442 Глава 16 Полет без моделирования физики Для написания простейших авиасимуляторов физика не нужна вовсе. Все, что нужно, - это программа, в которой точка, откуда вы видите окружающие объекты, перемещается соответственно передвижению самолета. Добавьте симпатичный окружающий мир и правдоподобное управление - и игра готова. Именно так создавались авиасимуляторы в течение многих лет. С помощью Direct3D написать такой авиасимулятор чрезвычайно просто. Платформа физического моделирования, описанная в этой книге, еще больше облегчает его написание. В главе 14 «Готовимся создавать игры» мы уже рассматривали программу, в которой камера двигалась над ландшафтом. Можно написать авиасимулятор, взяв эту программу в качестве основы. При этом физика нам не понадобится. СОЗДАНИЕ ПРОСТОГО АВИАСИМУЛЯТОРА Чтобы заложить основу для моделирования физических законов, которые мы рассмотрим далее, давайте создадим авиасимулятор, использующий минимум физики. В этом авиасимуляторе используется вид со стороны, то есть игрок будет видеть самолет, которым управляет. КЛАСС BASIC_FLYER Первое, что нужно для программы авиасимулятора - класс, описывающий самолеты. Он должен загружать сетчатую модель самолета. Как и машина в программе автосимулятора из предыдущей главы, самолет будет оставаться в центре экрана. Окружающие объекты будут двигаться относительно самолета, создавая впечатление, что самолет разворачивается, кренится и так далее. В листинге 16.1 приведено определение класса basic_flyer. Замечание Код класса basic_flyer есть на прилагающемся к книге компакт-диске. Он находится в файлах FMBasicFlyer.h и PMBasicFlyer.cpp в папке Source\Chapterl6\BasicFlyer. Класс basic_flyer является производным от класса rigid_body. Добавленные в нем элементы данных предназначены для выполнения маневров. Они задают начальную ориентацию самолета и его крен, тангаж и рыскание. Предупреждение Программа должна задать значения private-элементов данных initial- ForwardDirection и initialUpDirection после загрузки сетчатой модели, прежде чем начинать выполнять ее преобразования. Класс basic_ flyer должен знать начальную ориентацию сетчатой модели при ее загрузке, чтобы он смог правильно ее ориентировать в полете.
Авиация и космические корабли 443 Листинг 16.1. Класс basic_flyer 1 class basic_flyer : public rigid_body 2 { 3 private: 4 angle_set_3d turnAngles; 5 vector_3d initialForwardDirection; 6 vector_3d initialUpDirection; 7 8 public: 9 basic_fIyer() ; 10 11 void Pitch(scalar changelnPitch); 12 scalar Pitch(void); 13 14 void Yaw(scalar changelnYaw); 15 scalar Yaw(void); 16 17 void Roll(scalar changelnRoll); 18 scalar Roll(void); 19 20 void InitialForwardDirection( 21 vector_3d forward); 22 23 void InitialUpDirection( 24 vector_3d up); 25 26 vector_3d CurrentDirection(void); 27 28 vector_3d CurrentUpDirection(void); 29 30 virtual bool Update( 31 scalar changelnTime); 32 }; Методы, прототипы которых перечислены в строках 9-24, считывают или задают значения private-элементов данных. Кроме того, в классе Ьа- sic_f Iyer есть методы, вычисляющие текущую ориентацию и обновляющие данные о местоположении самолета. В этом классе нет метода Render (). Вместо него используется унаследованный метод Render (). Можно создать классы, производные от basic_f Iyer, в которых поворот будет связан с креном. Если нужно, можно не связывать эти операции - все зависит от того, какой летательный аппарат вы имитируете. Если вы пишете авиасимулятор, поворот должен сопровождаться креном. Если вы пишете космический симулятор, это не обязательно. Можно даже использовать класс basic_f Iyer для игры вроде Descent. В играх этой серии вы перемещались по системам пещер и туннелей, населенных враждебными роботами. Корабль, которым вы управляли,
444 Глава 16 мог независимо вращаться вокруг трех осей. Враги могли приближаться с любого направления. Не существовало понятия верха или низа. В течение некоторого времени Descent был моей любимой игрой. Мне нравилась доступная в нем свобода передвижения. Перемещаясь между домом и работой, я часто мечтал о транспортном средстве, которое могло бы двигаться так же свободно, как и корабль игрока в этих играх. Код методов CurrentDirection(), CurrentUpDirection() и Update () содержится в листинге PMBasicFlyer.cpp. Он приведен ниже в листинге 16.2. Назначение класса basic_flyer Класс basic_f Iyer устроен таким образом, чтобы изображенный летательный аппарат не точно имитировал поведение самолета. Это позволит использовать его в качестве базового для имитации самых разных летательных аппаратов. Если вы попробуете запустить программу с компакт-диска, то увидите, что смоделированный самолет не кренится при поворотах. Настоящие самолеты должны крениться, разворачиваясь. Да, программа позволяет накренить самолет, но для этого нужно нажимать другие клавиши. Листинг 16.2. Реализации методов CurrentDirection(), CurrentUpDirection() и Update() 1 vector_3d basic_flyer::CurrentDirection(void) 2 { 3 matrix4x4 rotationMatrix; 4 5 return ( 6 rotationMatrix.RotateXYZ( 7 initialForwardDirection, 8 rigid_body::CurrentOrientation())); 9 } 10 11 vector_3d basic_fIyer::CurrentUpDirection(void) 12 { 13 matrix4x4 rotationMatrix; 14 15 return ( 16 rotationMatrix.RotateXYZ( 17 initialUpDirection, 18 rigid_body::CurrentOrientation())); 19 } 20 21 bool basic_flyer::Update( 22 scalar changelnTime) 23 { 24 // Если летательный аппарат разворачивается...
Авиация и космические корабли 445 25 if ((!CloseToZero(turnAngles.XAngle())) || 26 (!CloseToZero(turnAngles.YAngle())) || 27 (!CloseToZero(turnAngles.ZAngle()))) 28 { 29 // 30 // Разворачиваем летательный аппарат. 31 // 32 angle_set_3d changelnAngle; 33 changelnAngle.SetXYZ( 34 rigid_body::CurrentOrientation{).XAngle() 35 + turnAngles.XAngle(), 36 rigid_body::CurrentOrientation().YAngle() 37 + turnAngles.YAngle(), 38 rigid_body::CurrentOrientation().ZAngle() 39 + turnAngles.ZAngle()) ; 40 41 rigid_body::CurrentOrientation(changelnAngle); 42 43 // 44 // Вращаем вектор скорости вместе с летательным 45 // аппаратом. 46 vector_3d newVelocity; 47 matrix4x4 rotationMatrix; 48 49 // Находим направление нового вектора скорости. 50 newVelocity = 51 rotationMatrix.RotateXYZ( 52 initialForwardDirection, 53 changelnAngle); 54 55 // Заменяем его на единичный вектор. 56 newVelocity = newVelocity.Normalize(SCALAR_TOLERANCE); 57 58 // Умножаем на текущую величину скорости. 59 newVelocity *= rigid_body::LinearVelocity().Norm(); 60 61 // Задаем скорость в правильном направлении. 62 rigid_body::LinearVelocity(newVelocity); 63 64 /* Сбрасываем в 0 углы поворота, готовясь к просчету 65 следующего кадра. */ 66 turnAngles.SetXYZ@.0,0.0,0.0) ; 67 } 68 69 return (rigid_body::Update(changelnTime)); 70 }
446 Глава 16 Чтобы реализовать метод CurrentDirection (), код которого приведен в листинге 16.2, я добавил к платформе классы matrix4x4 и vec- tor_4d, реализующие соответственно четырехмерные матрицы и векторы. Они понадобятся нам, чтобы выполнять преобразования в трехмерном пространстве. Точнее говоря, в методе CurrentDirection () объекты классов matrix4x4 и vector_4d используются для вращения вектора, указывающего вперед. Вспомните, что в классе basic_f Iyer есть вектор initialForwardDirection, в котором хранится изначальное направление вперед. Метод CurrentDirection () передает углы поворота, хранящиеся в классе rigid_body, от которого унаследован класс basic_f Iyer, методу RotateXYZ () класса matrix4x4. Метод RotateXYZ () вращает вектор, хранящийся в элементе initialForwardDirection, на углы, указанные методом rigid_body: : CurrentOrientation (). В результате получается вектор, указывающий в текущем направлении «вперед» летательного аппарата. Метод CurrentUpDirection () работает почти так же, как и метод CurrentDirection (). Единственное действительное отличие в том, что метод CurrentOpDirection () вращает вектор, указывающий направление «вверх» у летательного аппарата. Это направление не обязательно направлено вертикально вверх относительно поверхности земли. Если летательный аппарат выполняет бочку и находится в перевернутом состоянии, то вектор «вверх» будет указывать прямо в землю. Код метода Update () класса basic_flyer приведен в строках 21-70 листинга 16.2. Он поворачивает летательный аппарат из текущей ориентации на углы, указанные в private-элементе данных turn_angles. Кроме того, метод Update () вращает вектор скорости летательного аппарата. Это приводит к тому, что летательный аппарат непрерывно двигается в том направлении, в котором он ориентирован. Чтобы повернуть вектор скорости, метод Update () сначала находит направление, в котором ориентирован летательный аппарат. Затем он преобразует вектор направления в единичный вектор в строке 56. Затем метод Update () находит величину вектора скорости и умножает на нее единичный вектор направления. В строке 62 метод задает новый вектор поступательной скорости. Он также сбрасывает в 0 углы поворота в строке 66. Прежде чем метод заканчивает выполняться, он обновляет данные о местоположении летательного аппарата, вызывая метод rigid__body: : Update (). Использование класса basicflyer Создание авиасимулятора с помощью класса basic_f Iyer потребует всего пары сотен строк кода. Основные операции, которые нужно выполнить, таковы: 1. Инициализировать Direct3D. 2. Инициализировать геометрию сцены и объект класса Ьа- sic_flyer. 3. Обработать данные, вводимые пользователем.
Авиация и космические корабли 447 4. Обновить сцену соответственно введенным пользователем данным. 5. Обновить позицию камеры соответственно перемещению летательного аппарата. 6. Выполнить рендеринг сцены. Шаги 1, 4 и 6 не должны представлять для вас трудностей, поэтому мы сосредоточимся на шагах 2, 3 и 5. Код, который их выполняет, приведен в листинге 16.3. Листинг 16.3. Важные функции из файла FlightSim.cpp 1 void my_game::SetupGeometry() 2 { 3 // Задаем модель поверхности земли. 4 theGround.LoadMesh("ground.x"); 5 6 // Добавляем самолет. 7 thePlane.LoadMesh("plane.x"); 8 9 // 10 // Задаем свойства самолета. 11 // 12 thePlane.InitialForwardDirection( 13 vector_3d@.0,0.0,-1.0f)); 14 thePlane.InitialUpDirection{ 15 vector_3d@.0,1.Of ,0.0)) ; 16 thePlane.Location(vector_3d@.0,7.0,0.0)); 17 thePlane.BoundingSphereRadiusB.0); 18 thePlane.MassA000.Of) ; 19 thePlane.RotationalInertia( 20 vector_3dE000.0f,5000.Of,1000.Of)); 21 22 /* Разворачиваем самолет так, чтобы наблюдатель 23 видел его с хвоста. */ 24 thePlane.CurrentOrientation( 25 angle_set_3d( 26 0.0, 27 DegreesToRadiansA80.Of), 28 0.0)); 29 30 // Задаем скорость самолета в направлении его движения. 31 thePlane.LinearVelocity( 32 vector_3d@.0,0.0,30.0f)) ; 33 } 34 35 void my_game::SetViewPoint()
448 Глава 16 36 { 37 // Получаем единичный вектор в направлении движения самолета. 38 vector_3d planeDirection; 39 planeDirection=thePlane.CurrentDirection(); 40 planeDirection=planeDirection.Normalize(SCALAR_TOLERANCE); 41 42 vector_3d planeUpDirection; 43 planeOpDirection=thePlane.CurrentUpDirection(); 44 planeUpDirection= 45 planeUpDirection.Normalize(SCALAR_TOLERANCE); 46 // Указываем конкретную точку sa самолетом. 47 planeDirection *= 20; 48 49 // Размещаем камеру немного "выше" этой точки. 50 eyePoint = 51 thePlane.Location() - 52 planeDirection + 53 (planeUpDirection * 5); 54 55 lookatPoint = thePlane.Location(); 56 upDirection.x = planeUpDirection.X(); 57 upDirection.у = planeUpDirection.Y(); 58 upDirection.z = planeUpDirection.Z(); 59 60 D3DXMATRIX tempViewMatrix; 61 D3DXMatrixLookAtLH( 62 &tempViewMatrix,SeyePoint,SlookatPoint,SupDirection); 63 theApp.ViewMatrix(tempViewMatrix); 64 } 65 66 bool my_game::ProcessInput() 67 { 68 const int KEYBOARD_DATA_ARRAY_SIZE = 256; 69 BYTE keys[KEYBOARD_DATA_ARRAY_SIZE]; 70 static int accelMagnitude = 0; 71 static int decelMagnitude = 0; 72 static int leftMagnitude = 0; 73 static int rightMagnitude = 0; 74 75 vector_3d tempVector; 76 force tempForce; 77 78 HRESULT result = 79 theApp.Keyboard()-X3etDeviceState( 80 KEYBOARD_DATA_ARRAY_SIZE, 81 keys); 82 83 // Если данные считать не удалось... 84 if (result != DI_0K) 85 {
Авиация и космические корабли 449 86 result = theApp.Keyboard()->Acquire(); 87 while (result = DIERR_INPUTLOST) 88 { 89 result = theApp.Keyboard()->Acquire(); 90 } 91 92 /* Если приоритет принадлежит другому приложению 93 или произошла еще какая-нибудь ошибка... */ 94 if ((result = DIERRJDTHERAPPHASPRIO) || 95 (result == DIERR_NOTACQUIRED)) 96 { 97 // Просто попытаемся еще раз позже. 98 return (true); 99 } 100 } 101 // Если данные успешно считаны... 102 else 103 { 104 // Если нажата стрелка вверх... 105 if (DirectlnputKeyDown(keys[DIK_UP])) 106 { 107 // Штурвал отклонен назад. 108 thePlane.Pitch( 109 thePlane.Pitch() + 5); 110 ) 111 // Если нажата стрелка вниз... 112 else if (DirectlnputKeyDown(keys[DIK_DOWN])) 113 { 114 // Штурвал отклонен вперед. 115 thePlane.Pitch( 116 thePlane.Pitch() - 5); 117 } 118 119 // Если нажата стрелка влево... 120 if (DirectlnputKeyDown(keys[DIK_LEFT])) 121 { 122 // Штурвал отклонен влево. 123 thePlane.Yaw( 124 thePlane.Yaw() - 5); 125 } 126 // Если нажата стрелка вправо... 127 else if(DirectlnputKeyDown(keys[DIK_RIGHT])) 128 { 129 // Штурвал отклонен вправо. 130 thePlane.Yaw( 131 thePlane.Yaw() + 5); 132 ) 133
450 Глава 16 134 // Если нажата клавиша А... 135 if (DirectlnputKeyDown(keys[DIK_A])) 136 { 137 // Нажата педаль левого руля. 138 thePlane.Roll( 139 thePlane.Roll() + 5); 140 } 141 // Если нажата клавиша S... 142 else if (DirectlnputKeyDown(keys[DIK_S])) 143 { 144 // Нажата педаль правого руля. 145 thePlane.Roll( 146 thePlane.Roll() - 5) ; 147 } 148 } 149 150 return (true); 151 } Листинг 16.3 начинается с метода SetupGeometry (). Этот метод инициализирует трехмерное окружение, в котором будет двигаться самолет. Он также загружает модель самолета. Как и в примерах программ из предыдущих глав, в этом примере нет обработки ошибок. Это позволяет максимально упростить код. В реальных программах обязательно нужно предусмотреть обработку ошибок. После того, как метод SetupGeometry () загрузит элементы сцены и самолет, он задает свойства самолета. В строках 12-13 метод задает направление, в котором ориентирована сетчатая модель самолета при загрузке. Направление «вверх» самолета задается в строках 14-15. Эта инициализация должна быть выполнена, чтобы класс basic_f Iyer правильно работал. В строках 16-58 листинга 16.3 метод SetupGeometry () инициализирует основные физические параметры, необходимые для моделирования движения. Он не прикладывает к самолету никаких сил. Операторы в строках 24-28 разворачивают самолет в сторону от наблюдателя. В строках 31-32 задается скорость самолета. Метод SetViewPoint () начинается в строке 35 листинга 16.3. Этот метод использует векторы ориентации самолета, чтобы определить местоположение камеры. Он помещает камеру сзади и немного выше самолета. Затем метод направляет камеру на центр масс самолета. Вы, возможно, удивитесь, почему камера размещается немного выше самолета. Такое размещение камеры позволяет игроку лучше видеть самолет. Если бы камера была прямо сзади самолета, игрок видел бы только его хвост и крылья - большая часть фюзеляжа была бы не видна.
Авиация и космические корабли 451 Подсказка При написании программ, более сложных, чем примеры в этой книге, нужно обращать внимание на мелкие детали, вроде размещения камер. Впечатления и мнения игроков о ваших играх зависят от таких мелких деталей. Последний метод в листинге 16.3 - это метод Processlnput (). Нас интересует часть этого метода, начинающаяся в строке 105. Именно здесь начинается обработка действий пользователя. В программе нажатие стрелки вниз соответствует движению штурвала вперед, а нажатие стрелки вверх - движению штурвала назад. Это управление тангажом самолета. Стрелки влево и вправо, нажатия на которые обрабатываются начиная со строки 120, управляют рысканием самолета. В строках 136-148 обрабатываются нажатия на клавиши А и S, управляющие креном самолета. Использование этих клавиш позволит вам поднимать и опускать нос самолета, разворачивать его и управлять креном. В настоящих самолетах для управления креном используются как штурвал, так и педали. Однако во многих авиамоделях крен и поворот выполняются одновременно при нажатии стрелок влево или вправо. Это позволяет самолету реалистично крениться при разворотах. Кроме того, это упрощает управление самолетом для игроков, которые не имеют опыта управления настоящими самолетами. Физика самолетов Современные авиасимуляторы гораздо сложнее и реалистичнее того, который мы только что рассмотрели. Почти во всех этих программах используются хотя бы простейшие физические модели. Во многих авиасимуляторах используются технические данные реальных самолетов. В некоторых из них можно создавать собственные самолеты. Можно прикрепить крылья Боинга 747 к Цессне и посмотреть, что получится (большая подъемная сила!). Чтобы создать авиасимулятор, основанный на реальных физических моделях, программисты должны разбираться в устройстве самолетов и силах, действующих на них. Основные части самолета Самолет — это не только крылья. Он состоит из множества частей, которые должны слаженно работать, чтобы поднять самолет в воздух. На рисунке 16.2 изображены самые важные компоненты самолета. У каждого самолета есть фюзеляж. Это основная часть самолета. В фюзеляже размещен кокпит, в котором сидит пилот. На маленьких самолетах кокпит часто находится над крыльями. Центр массы самолета находится на линии симметрии фюзеляжа. Он размещается где-то между крыльями.
452 Глава 16 Источник тяги Элерон Руль высоты Закрылок Фюзеляж Хвостовое оперение/рули Рис. 16.2. Основные составляющие самолета Предупреждение Можно (хотя и опасно) загрузить самолет так, что его центр массы будет находиться не между крыльями. Но в таком состоянии самолеты ОЧЕНЬ неустойчивы. Большая часть веса самолета должна находиться между крыльями или на крыльях. Крылья бывают разных форм. У маленьких самолетов крылья обычно прямоугольные, если смотреть сверху. У более скоростных самолетов крылья имеют стреловидную или треугольную форму, поскольку такая форма уменьшает турбулентность воздуха, появляющуюся при больших скоростях полета. У некоторых экспериментальных самолетов крылья
Авиация и космические корабли 453 имеют обратную стреловидность. Такие крылья резко увеличивают маневренность самолетов. Но такие самолеты всегда неустойчивы, и чтобы ликвидировать эту неустойчивость, встроенные компьютеры непрерывно отслеживают их поведение в воздухе. Это может сделать только компьютер - человек-пилот не сможет реагировать достаточно быстро, чтобы справиться с этой задачей на сверхзвуковых скоростях полета. На рисунке 16.2 показаны управляющие поверхности самолета, прикрепленные к его крыльям. При взлете и посадке самолет выпускает закрылки. Закрылки - это управляющие поверхности, создающие дополнительную подъемную силу. Кроме того, они увеличивают сопротивление воздуха движению самолета. Это дополнительное сопротивление очень полезно при посадке - оно снижает скорость самолета. Обычно закрылки отклоняются вниз на угол от 30° до 60°. При взлете они отклоняются вверх, но на меньший угол - обычно меньше 45°. Чем меньше они отклонены от горизонтали, тем меньше создаваемая ими подъемная сила, но и тем меньше сопротивление воздуха. А сопротивление воздуха при взлете должно быть как можно меньше. Элероны тоже крепятся к крыльям, как показано на рисунке 16.2. Они управляют креном самолета. Если левый элерон отклоняется вверх, правый отклоняется вниз и наоборот. Источник тяги тянет самолет вперед. В качестве источника тяги может использоваться пропеллер, воздушно-реактивный или даже ракетный двигатель. У маленьких самолетов пропеллер обычно находится впереди фюзеляжа, и он тянет самолет за собой. Но на некоторых моделях пропеллер находится в хвосте и толкает самолет, а не тянет. Источники тяги могут размещаться и на крыльях самолета. Пилоты изменяют угол тангажа самолета с помощью рулей высоты. Эти рули могут размещаться в носовой части фюзеляжа, но чаще всего они размещаются на хвостовом оперении, как показано на рисунке 16.2. Иногда горизонтальное хвостовое оперение самолета делается цельнопо- воротным, то есть состоит только из рулей высоты. И, наконец, на вертикальном хвостовом оперении расположены рули направления. Они управляют рысканием самолета. Как и горизонтальное хвостовое оперение, вертикальное тоже может быть цельноповорот- ным, целиком образуя рули направления. Изобретение управляющих поверхностей В эпоху зарождения авиации пилоты летали на планерах, не оснащенных двигателями. Современные буксируемые планеры - отдаленные потомки тех первых летательных аппаратов тяжелее воздуха. Орвиль и Вильбур Райты, вдохновленные этими первыми попытками полета, решили попытаться создать оснащенный двигателем летательный аппарат. Во время первых испытаний братья Райт быстро обнаружили, что их аппарату нужен руль, похожий на киль, используемый в лодках. Кроме того, стала очевидной необходимость руля высоты. Но до появления идеи управления креном прошло довольно много времени.
454 Глава 16 Изучая особенности полета на самолетах, оснащенных двигателями, братья Райт обнаружили, что управлять креном необходимо по двум причинам. Во-первых, порывы бокового ветра могут накренить самолет или перевернуть его, если у пилота не будет возможности компенсировать крен. Во-вторых, попытки поворачивать самолет без крена приводят к неровному, неустойчивому движению. Такие повороты опасны. Однажды, раздумывая над этими проблемами, Вильбур держал в руках небольшую коробку Коробка была длинной, узкой и невысокой, немного напоминая крыло самолета. Рассматривая коробку, Вильбур согнул ее так, что один ее угол приподнялся, а противоположный опустился. Так родилась идея изгибания крыла. С помощью веревок, изгибавших концы крыла, Райты смогли управлять креном самолета. Перед первыми полетами на своем самолете они запатентовали идею управления самолетом с помощью изгибания крыла. Вскоре после первых полетов братьев Райт, группа под названием Aerial Experiment Association создала самолет под названием Red Wing. У него не было управления креном, и он не использовал изгибание крыла. Участники проекта знали о патенте братьев Райт и решили создать что-то другое, чтобы не зависеть от этого патента. Они решили прикрепить на шарнирах подвижные поверхности к задней кромке крыла. Эти поверхности пилот мог поднимать и опускать с помощью тросов Позднее эти поверхности назвали элеронами (от французского слова, означающего «маленькие крылья»). Вскоре стало понятно, что использовать их гораздо удобнее, чем изгибать крылья. Позднее Aerial Experimental Association построила новый самолет White Wing, ставший первым самолетом, на котором использовались элероны. К началу первой мировой войны на крыльях самолетов появились и закрылки. Хотя со времени создания первых самолетов прошло больше века, рули направления и высоты, элероны и закрылки используются до сих пор. Основные силы Самолет летит под действием сил, прикладываемых к нему окружающей средой, и сил, которые он сам прикладывает к этой среде. Двигатель самолета развивает усилие, тянущее самолет вперед. Это усилие называется тягой (thrust). При наборе скорости на самолет начинают действовать подъемная сила и сила сопротивления воздуха. Подъемная сила самолета должна превышать силу тяжести - еще одну силу, действующую на самолет. СИЛА ТЯЖЕСТИ И ПОДЪЕМНАЯ СИЛА В главе 11 «Сила тяжести и метательные снаряды» рассматривалась сила тяжести и ее воздействие на объекты, находящиеся вблизи поверхности Земли. На самолеты сила тяжести действует точно так же, как и на любые другие объекты.
Авиация и космические корабли 455 Она выражается простой формулой F = mg и приложена к центру масс самолета. Объяснить, что такое подъемная сила, куда сложнее. Эта сила возникает вследствие разности давления воздуха на нижнюю и верхнюю поверхности крыльев. На рисунке 16.3 показано, как возникает эта разность. Рис. 16.3. Возникновение подъемной силы На рисунке 16.3 показано поперечное сечение крыла. При движении этого крыла в воздухе воздух обтекает его снизу и сверху. Из-за формы крыла воздух, обтекающий его сверху, должен пройти более длинный путь, чем воздух, обтекающий его снизу. В результате воздух над крылом движется быстрее. Соответственно, его давление меньше, чем давление воздуха под крылом. Из-за разности давлений возникает подъемная сила. Чтобы найти подъемную силу, в играх можно использовать формулу L = 0.5CLpv2S В этой формуле L - это подъемная сила, S - площадь сечения крыла, v - скорость воздуха, ар- плотность воздуха. CL - это коэффициент подъемной силы. Этот коэффициент свой для каждой формы крыла. Инженеры определяют его, испытывая крылья в аэродинамических трубах. Проще всего подобрать этот коэффициент для игр, немного поэкспериментировав. Из жизненного опыта вы, вероятно, знаете, какие формы крыльев обладают большей подъемной силой. Пока формула для вычисления подъемной силы дает большую подъемную силу для более эффективного крыла, все в порядке. Поэкспериментируйте с игрой и подберите подходящие значения коэффициента подъемной силы. Подсказка Не бойтесь использовать в играх приближенные значения или относительные результаты, если они избавляют вас от большого объема работы. Например, не обязательно использовать в играх точные коэффициенты подъемной силы, используемые при проектировании реальных самолетов Если самолеты в игре ведут себя правильно относительно друг друга и выглядят правдоподобно, все будет в порядке.
456 Глава 16 ТЯГА И СОПРОТИВЛЕНИЕ ВОЗДУХА Тяга определяется типом установленного на самолете двигателя (или двигателей). Можно найти в Интернете данные о реально существующих двигателях и использовать их в программе, а можно просто придумать свои данные. Во втором случае опять придется экспериментировать и подбирать подходящие значения параметров. Сопротивление воздуха - это вычисляемое значение. Оно зависит от скорости движения самолета и площади сечения крыла. То же движение воздуха, которое объясняет появление подъемной силы, вызывает и трение. Сопротивление воздуха, вызванное этим трением, прикладывает к самолету силу, направленную против направления его движения. Сопротивление воздуха увеличивается с ростом угла атаки, то есть чем больше наклонено крыло к направлению движения самолета, тем больше будет сопротивление воздуха. Если крыло наклонено слишком сильно, сопротивление воздуха станет больше подъемной силы, и самолет замрет в воздухе. Обычно это очень плохо - за таким замиранием следует вертикальная посадка с очень большой скоростью. Пассажиры приходят в нездоровое возбуждение при мысли о такой посадке. Сопротивление воздуха вычисляется по формуле, похожей на формулу для вычисления подъемной силы: D = 0.5Cr/)v2S В этой формуле D есть сила сопротивления воздуха, a CD — коэффициент этого сопротивления. Этот коэффициент никогда не должен превышать половины коэффициента подъемной силы. Скорее всего он должен быть меньше трети коэффициента подъемной силы. Сопротивление воздуха определяется не только крыльями самолета, но и всеми другими его компонентами. Точное вычисление сопротивления воздуха - это задача для авиаконструкторов, а не для создателей компьютерных игр. В играх его можно вычислять приближенно по формулам, приведенным в главе 15 «Автомобили, корабли и лодки». Эти формулы повторены ниже в таблице 16.1. Таблица 16.1. Вычисление силы сопротивления воздуха Формула Описание Эту формулу можно использовать, если самолет движется недостаточно быстро, чтобы вызвать заметную турбулентность воздуха Эту формулу нужно использовать при моделировании высокоскоростных самолетов, движение которых вызывает заметную турбулентность, т. е. завихрение воздуха ffd = _cfdv FFD = -CFDv2
Авиация и космические корабли 457 Моделирование летательных аппаратов: правильное приложение сил Моделирование летательных аппаратов в программах в основном сводится к приложению правильных сил к правильным точкам. Тягу двигателя и силу тяжести нужно прикладывать к центру масс. Это позволит рассматривать летательный аппарат как твердое тело. А как насчет подъемной силы? Нужно ли прикладывать ее к каким-то точкам на крыльях или тоже к центру масс? Ответ на этот вопрос зависит от реализации летательного аппарата в программе. Если реализация проста, лучше поместить центр массы в точке, в которой пересекаются продольная ось симметрии летательного аппарата и средняя линия крыльев. В такой реализации подъемную силу можно прикладывать к центру масс. Когда выпущены закрылки, они создают дополнительные подъемную силу и сопротивление воздуха. В большинстве игр эти силы тоже прикладывают к центру масс. Но в очень реалистичных авиасимуляторах, в которых может имитироваться невыпуск закрылок, создаваемые закрылками подъемная сила и сопротивление воздуха могут прикладываться к срединным точкам закрылок. Однако в подавляющем большинстве авиасимуля- торов эти силы тоже прикладываются к центру масс. Если элероны отклонены вверх или вниз, на них давят воздушные потоки. Это давление приводит к возникновению силы, направленной соответственно вниз или вверх. Эта сила создает момент, прикладываемый к центру масс самолета, вследствие чего самолет начинает вращаться вдоль продольной оси. Чтобы смоделировать такое поведение, силы, вызываемые отклонением элеронов, нужно прикладывать к их срединным точкам. Рули направления — это просто элероны, установленные вертикально. Их можно моделировать так же, как элероны. Отклонение рулей направления вызывает появление силы, направленной влево или вправо. Эту силу нужно прикладывать к срединной точке рулей. Физика космических кораблей Моделирование движения космических кораблей может быть очень простой или очень сложной задачей - в зависимости от того, какие космические корабли вы хотите изобразить в игре. Если игрок пилотирует космический истребитель, то все очень просто. Но если он взлетает с Земли на ракете и высаживается на Луну в посадочном модуле (Lunar Module - LM), вам понадобится изрядный объем физических знаний, чтобы точно изобразить происходящее. Перестрелки в космосе Я был подростком, когда вышел фильм Star Wars. Он был непохож ни на что, существовавшее ранее. Я был настолько увлечен им, что смотрел его
458 Глава 16 четыре раза за первый месяц после его выхода. Одна из лучших частей в фильме - атака Люка Скайуокера против Звезды Смерти. Спустя несколько лет в играх для персональных компьютеров вы могли сыграть роль пилота космического истребителя и выполнить эту атаку сами. Кроме того, появился сериал Wing Commander, в котором игрок выступал в роли человека-пилота, участвующего в войне с инопланетянами. В последнее время игр такого рода стало меньше, вероятно, потому, что их стало слишком просто делать. Если вы хотите написать такую игру сами, это можно легко сделать, используя класс basic_f Iyer, который мы рассмотрели ранее в этой главе. Вам не понадобится моделировать законы физики. В глубоком космосе нет силы тяжести, тянущей истребитель вниз - если только вы не приблизитесь к планете. В большинстве игр этого сделать нельзя. Чаще всего вы просто двигаетесь относительно других кораблей в отсутствие внешних сил. Для моделирования такой ситуации отлично подходит класс basic_f Iyer. Ракеты Во всех реальных космических аппаратах, созданных на сегодняшний день, используются ракеты. Создать игру, в которой изображены построенные на ракетах космические аппараты, гораздо сложнее, чем игру, изображающую космические истребители. Для этого нужно разбираться в принципах работы ракет и силах, которые возникают в работающих ракетах. Кроме того, нужно знать, как выполняются выходы на орбиту и межпланетные перелеты. КАК РАБОТАЮТ РАКЕТЫ Ракеты, использующие в качестве топлива водород и кислород, - это, по сути, просто большие трехкамерные термосы. Да, да. Общая схема таких ракет изображена на рисунке 16.4. Внутри ракеты есть три камеры, показанные на рисунке 16.4. Каждая из них теплоизолирована, почти так же, как обычный термос. Теплоизоляция нужна потому, что жидкие кислород и водород хранятся при очень низких температурах. Именно низкая температура делает их жидкими. При комнатной температуре и кислород, и водород - газы. Самое важное для нас сейчас свойство кислорода и водорода в том, что они могут реагировать между собой. При сгорании водорода в кислороде образуется вода и выделяется очень много энергии. Именно эта реакция и происходит в камере сгорания ракеты. Из-за разогрева образующийся при сгорании водяной пар сильно расширяется и с большой скоростью вылетает из камеры сгорания через сопло ракеты. Ракета начинает двигаться. Реакция, протекающая в камере сгорания ракеты, прикладывает давление ко всем стенкам этой камеры. Но дна у камеры нет, поэтому продукты сгорания устремляются вниз. А в соответствии с третьим законом Ньютона, каждое действие рождает равное противодействие. Истечение газов через сопло ракеты толкает ракету в обратном направлении. Поехали!
Авиация и космические корабли 459 Жидкий кислород Жидкий водород Камера сгорания Сопло Рис. 16.4. Основные компоненты ракеты ВЫХОД НА ОРБИТУ Чаще всего ракеты используются для вывода на орбиту людей и других грузов. В технике «люди и другие грузы», загруженные в ракету, называются ее полезной нагрузкой (payload). Ракета выводит свою полезную нагрузку на орбиту. Если мы пишем игру, в которой изображается запуск ракет на орбиту, нам нужно разобраться, как ракеты выходят на орбиту. В главе 11 мы рассматривали действие силы тяжести на метательные снаряды. При этом мы ограничились рассмотрением метательных снарядов вблизи поверхности Земли. Орбиты можно изучить, несколько расширив рассматриваемую область, например, так. Представьте себе, что вы стреляете из пушки, установленной под углом 45°. Вылетевший из нее снаряд опишет в воздухе параболу. А что произойдет, если зарядить пушку очень мощным порохом, который придаст снаряду огромную скорость? Не обращайте внимания на то, что пушку, скорее всего, разорвет при выстреле. Попытаемся представить себе, что произойдет со снарядом. Что будет, если снаряд разгонится до такой скорости, что сможет пролететь тысячи километров? На таких расстояниях уже нельзя не принимать во внимание кривизну поверхности Земли. Проще говоря, нужно учитывать, что поверхность будет искривляться, уходя от снаряда. Если скорость снаряда достаточно велика, он будет падать к земле так же быстро, как земля будет уходить от него.
460 Глава 16 Я понимаю, что это очень упрощенный пример. Но он позволяет проиллюстрировать концепцию орбиты. С точки зрения физики, объекты удерживаются на орбитах вокруг планет или звезд, если действующая на эти объекты центростремительная сила тяготения уравновешивается тангенциальной силой, под воздействием которой объекты движутся по орбитам. Чтобы выйти на орбиту, ракета должна совершить определенную работу. Здесь работа - это не работа в обычном, повседневном значении этого слова. Термин «работа» в физике имеет особое значение. Он обозначает энергию, которую нужно затратить, чтобы изменить состояние физической системы. Вот пример, иллюстрирующий концепцию работы. Представьте себе, что вы стоите в чистом поле и подбрасываете мяч вертикально вверх. Заставляя мяч двигаться вверх, вы тратите определенное количество энергии или, что то же самое, совершаете определенную работу. Когда брошенный мяч поднимается вверх, его кинетическая энергия уменьшается, а потенциальная - растет. Чтобы понять, что такое потенциальная энергия, продолжайте смотреть на мяч, пока он летит. Когда он прекратит подниматься, его потенциальная энергия будет максимальной - он затратит ее, упав вниз и ударив вас по голове. Приблизительно так же работают все системы, в которых действует гравитация. В гравитационном поле необходимо затрачивать энергию, чтобы поднимать объект. Потенциальная энергия объекта будет нарастать по мере подъема. Она будет высвобождаться, когда объект будет падать вниз. Точнее говоря, потенциальная энергия будет превращаться в кинетическую. Количество кинетической энергии будет равно количеству работы, совершенной при подъеме объекта. Соответственно, мы можем записать: W = -U AU + АК = 0 Эти формулы говорят нам, что потенциальная энергия, которую приобретет объект при подъеме, равна работе, затраченной на его подъем. Чем выше мы поднимем объект, тем большую работу мы совершим, и тем больше будет накопленная потенциальная энергия. Кроме того, формулы показывают нам, что общее изменение энергии системы равно 0, то есть с увеличением потенциальной энергии кинетическая энергия будет уменьшаться. Люди знают это из повседневного опыта. Мы все видели, что подброшенный мяч будет замедлять свой подъем. Это значит, что его кинетическая энергия уменьшается, а потенциальная - растет. Когда мяч начнет падать, кинетическая энергия начнет расти, а потенциальная - падать. При столкновении с землей кинетическая энергия будет максимальной, а потенциальная - нулевой. Работу, которую нужно совершить для поднятия объекта, например, ракеты, на заданную высоту, можно найти по такой формуле: л„ GMm W — г
Авиация и космические корабли 461 Здесь G - универсальная гравитационная постоянная, равная 6.67 х Ю-11 (Н х м2)/кг2, М — масса Земли (или планеты, которую мы рассматриваем), m - масса ракеты, г - высота подъема ракеты над Землей. Из этой формулы для работы мы можем получить формулу для силы, совершающей работу при подъеме ракеты: А f=-au ( GMm'( GMm Дг Лг г2 Потенциальная энергия системы равна совершенной над ней работе, поэтому изменение в потенциальной энергии равно изменению в работе. Потенциальная энергия изменяется с изменением расстояния до Земли. Соответственно, сила притяжения между Землей и ракетой обратно пропорциональна квадрату расстояния между ними. Орбиты могут быть круговыми или эллиптическими. Чтобы не усложнять себе задачу, давайте рассматривать только круговые орбиты. Вот формула центростремительной силы при равномерном круговом движении: F = mvrr В этой формуле vr — это радиальная скорость, то есть скорость, с которой спутник падает по направлению к Земле. ПРЕБЫВАНИЕ НА ОРБИТЕ Работу, которую нужно совершить против центростремительной силы, действующей на спутник, можно приравнять к его потенциальной энергии: -GMm =mv 2 r2 —r Из этого уравнения можно получить такое выражение для v : GM Время, необходимое для совершения одного оборота по орбите, будет выражаться родственной формулой: 4яУ GM Здесь Т — длительность одного оборота по орбите.
462 Глава 16 А как найти тангенциальную скорость? Она нужна нам, чтобы моделировать движение объекта по орбите. Вот формула, по которой можно ее найти: GM И радиальная, и тангенциальная скорость представляют собой векторы. Векторы можно сложить: vs=vt+vr Эта формула поясняет то, что изображено на рисунке 16.5. Орбитальная скорость спутника есть сумма его радиальной и тангенциальной скоростей. Vt+Vr Рис. 16.5. Орбитальная скорость спутника Получив возможность вычислять силы, действующие на спутник, и скорости, необходимые для движения по орбите, можно адаптировать класс basic_f Iyer для моделирования движения космических объектов. Замечание Орбитальную скорость (скорость движения спутников по орбите вокруг Земли) иногда называют первой космической. Она приблизительно равна 8 км/с. К ДРУГИМ ПЛАНЕТАМ Чтобы отправиться на другие планеты, космический аппарат должен разогнаться как минимум до второй космической скорости, которую называют также параболической скоростью или скоростью убегания. Разогнавшись до этой скорости, космический аппарат преодолеет действие силы тяготения, удерживающей его на орбите. Чтобы найти вторую космическую скорость, можно воспользоваться теми же уравнениями, что и для вычисления первой космической скорости. Начнем с того, что работа, необходимая для подъема объекта вверх, равна его потенциальной энергии:
Авиация и космические корабли 463 W = К Заменив в этой формуле W на соответствующее выражение, получим: GMm Jv Но у нас уже есть формула, позволяющая найти кинетическую энергию падающего объекта: Подставим Теперь мы это выражение можем найти v: К= mv2 2 в предыдущее: mv2 2 v=. GMm г /2GM Если подставить в эту формулу параметры Земли, получим результат: вторая космическая скорость составляет около 11.2 км/с или 40 200 км/ч. Высадка на Луну Ракеты могут выводить груз на орбиту или отправлять его к другим целям. Но чтобы высадиться на других планетах, понадобятся специальные корабли. В программе Аполлон использовался посадочный модуль Lunar Excursion Module (LEM), который позже переименовали в Lunar Module (LM). В некоторых играх изображалась посадка LM на Луну. Физика, которую при этом нужно использовать - это просто физика твердого тела. Находясь на орбите, LM включает свои двигатели, чтобы снизить скорость и начать снижаться на Луну. В игре LM можно представить как объект класса, производного от rigid_body. С уменьшением тангенциальной скорости LM его радиальная скорость будет все больше приближать LM к Луне. Чем ближе LM будет к Луне, тем больше будет его радиальная скорость. Игрок должен будет управлять LM, поворачивая его и включая двигатели, чтобы снизить его скорость. При посадке нужно будет развернуть LM так, чтобы тяга двигателей противодействовала силе тяжести и позволила совершить мягкую посадку.
464 Глава 16 Замечание Сила тяжести на Луне приблизительно в 6 раз меньше, чем на Земле. Поэтому вместо g в формулах можно будет использовать д/6, если речь идет о Луне. Вот и все. Имитация LM - это просто еще одно применение физики твердых тел. Путешествия на другие планеты с помощью известных методов Во многих фантастических рассказах и фильмах рассказывается о путешествиях на другие планеты. Исходя из современных физических представлений, такое путешествие можно совершить несколькими способами. ИОННЫЕ ДВИГАТЕЛИ Ионный двигатель - это чрезвычайно экономичный с точки зрения расхода топлива способ путешествия на другие планеты. Он работает за счет сообщения газу электрического заряда, то есть ионизации этого газа. Необходимое для этого электричество можно получать от солнечных батарей. В качестве топлива используется газ ксенон. Сообщив атомам ксенона отрицательный электрический заряд, можно разогнать их с помощью электромагнитов или электростатического поля. Поток ксенона будет прикладывать усилие к космическому аппарату точно так же, как струя обычного реактивного двигателя. Тяга ионных двигателей невелика - она примерно равна весу листа бумаги. Но поскольку энергия, необходимая для работы двигателя, получается от солнечных батарей, для космического аппарата с ионным двигателем нужно намного меньше топлива, чем для аппарата с химическими двигателями. Постоянно работающий ионный двигатель постепенно разгонит аппарат до скоростей, достаточных для межпланетных перелетов и даже для выхода за пределы солнечной системы. Моделирование космического аппарата с ионными двигателями - это еще одна область применения физики твердых тел. Двигатель постоянно прикладывает к телу небольшое усилие. Если это усилие прикладывается на протяжении длительного времени, корабль может набрать большую скорость и уйти в глубокий космос. Подсказка При моделировании космического аппарата с ионными двигателями нужно постепенно медленно уменьшать его массу, поскольку он медленно расходует топливо. Это важно. Если масса аппарата будет оставаться постоянной, вы получите неточные результаты.
Авиация и космические корабли 465 ПЛАЗМЕННЫЕ ДВИГАТЕЛИ Теоретически можно путешествовать на другие планеты на корабле, оснащенном плазменными двигателями. Идея довольно проста. Прикрепляем к кораблю длинную балку. На конце этой балки помещаем ядерный реактор, похожий на те, что используются на Земле. Топливо — обычная вода. Реактор разогревает воду, пока она не превращается в плазму. Затем двигатель выстреливает плазму в одном направлении. Корабль начинает двигаться в противоположном направлении. У таких двигателей есть и преимущества, и недостатки. Одно из преимуществ - большая тяга. Корабль будет разгоняться так же быстро, как и с помощью химических двигателей. Второе преимущество в том, что заправиться можно везде, где есть вода. Например, кольца Сатурна в значительной степени состоят из воды. Вот вам и первый недостаток. Если таких кораблей будет много, то мы постепенно израсходуем на их заправку все кольца Сатурна. А как же охрана окружающей среды? Второй недостаток в том, что поток плазмы, вылетающий из двигателя, будет радиоактивным. Так что не стоит запускать такой корабль с Земли. Если нам будет нужно его построить, лучше сделать это за орбитой Луны и запускать оттуда. Замечание На корабле Discovery из фильма 2001: A Space Odyssey использовались плазменные двигатели. Как и моделирование аппаратов с ионными двигателями, моделирование кораблей с плазменными двигателями требует применения физики твердых тел. КОРАБЛИ С МАГНИТНЫМИ ЛОВУШКАМИ Для межзвездных путешествий лучше подходят корабли, в которых запас топлива пополняется за счет магнитной ловушки. На переднем конце такого корабля крепится огромная воронка, которая за счет использования электромагнитов собирает атомы водорода из пространства, сквозь которое летит корабль. Межзвездное пространство заполнено очень разреженным водородом. Магнитная ловушка собирает этот водород, и он используется в качестве топлива в термоядерном реакторе. Ток, генерируемый реактором, питает электромагниты ловушки. Другие электромагниты разгоняют продукты синтеза в реакторе до огромных скоростей и выбрасывают их назад в виде сфокусированного потока. Этот поток толкает корабль вперед. Поскольку водорода в межзвездном пространстве очень мало, такой корабль не сможет собирать его в достаточном количестве для функционирования, если он будет двигаться со скоростью меньше нескольких процентов от световой. Скорость света равна 300 000 км/с. Однако если корабль разогнать так, что он начнет функционировать, он сможет функционировать практически бесконечно.
466 Глава 16 Замечание На таком принципе работал корабль Red Dwarf из одноименного британского фильма. Именно этот принцип позволил ему функционировать в течение трех миллионов лет. ЗАМЕДЛЕНИЕ ТЕЧЕНИЯ ВРЕМЕНИ Чтобы достичь даже ближайших звезд за приемлемые промежутки времени, космические корабли должны перемещаться со скоростями, близкими к скорости света. При движении с такими скоростями становится заметным эффект замедления течения времени на корабле по сравнению с течением времени для неподвижного объекта. Чтобы понять, почему так происходит, давайте проведем воображаемый эксперимент, предложенный Альбертом Эйнштейном. Представьте себе лазерный дальномер, каждую секунду посылающий к зеркалу короткий импульс света. Свет отражается от зеркала и возвращается назад, к детектору, закрепленному на лазере. На рисунке 16.6 показана структура такого дальномера. ШЬЛШИЕдМНЦИИ Зеркало Детектор Лазер Рис. 16.6. Свет от лазерного дальномера распространяется практически по прямой Представьте себе, что дальномер находится на космическом корабле. У наблюдателя на Земле есть такой же дальномер, синхронизированный с помещенным на корабле. Когда корабль начинает двигаться, наблюдатель на Земле увидит, что свет дальномера, помещенного на корабле, будет проходить более длинный путь, чем свет дальномера, оставшегося на Земле. На рисунке 16.7 изображен путь света из дальномера на корабле, каким этот путь виден с Земли.
Авиация и космические корабли 467 Изображенный на рисунке 16.7 дальномер находится на корабле, двигающемся слева направо. Лазер испускает световой импульс, находясь в позиции А. Ко времени, когда импульс достигает зеркала, корабль смещается в позицию В, а ко времени, когда он вернется к детектору - в позицию С. Человек, находящийся на этом корабле, будет видеть лазерный импульс движущимся снизу вверх к зеркалу и возвращающимся к детектору, как на рисунке 16.6. Поскольку корабль движется слева направо, неподвижный наблюдатель увидит лазерный импульс смещающимся слева направо, а не только движущимся вверх и вниз. Для этого наблюдателя траектория импульса будет такой, как показанная на рисунке 16.7. Рис. 16.7. Путь лазерного импульса на корабле А теперь самое важное. Скорость света - это постоянная величина, она не может изменяться. И что из того? Взгляните еще раз на рисунок 16.7. Свет дальномера, помещенного на корабле, движется по зигзагу с точки зрения неподвижного наблюдателя. В результате наблюдатель посчитает, что этот свет прошел больший путь, чем свет неподвижного дальномера. Свет не может увеличить свою скорость, чтобы пройти больший путь за фиксированный интервал времени, поскольку его скорость постоянна, независимо от того, где он излучается или принимается. Единственная возможность для света пройти больший путь объясняется замедлением течения времени. Да, скорость света постоянна, поэтому время должно растягиваться. Это значит, что люди на корабле, летящем с околосветовой скоростью, будут стареть медленнее, чем оставшиеся на Земле. Время на нем будет идти медленнее, чем на Земле. Разницу прошедших интервалов времени можно найти по формуле: корабля Земли
468 Глава 16 В этой формуле At^num ~ это интервал времени, прошедший для наблюдателя на Земле. Значение At абдя - это интервал времени, прошедший для наблюдателя на корабле, v — это средняя скорость движения корабля за интервал времени At абля. с - это скорость света C00 000 км/с). В сюжетах игр, в которых моделируются межзвездные путешествия, можно использовать факт замедления течения времени. Например, капитан корабля может посетить одно и то же место, причем сам он постареет на несколько лет, а в этом месте пройдут тысячелетия. Межпланетные путешествия согласно предполагаемым возможностям Во многих фантастических произведениях описываются ускоренные методы перемещения, основанные на воображаемых или предполагаемых физических законах. Но если эти законы не противоречат известным нам из повседневной жизни, то читатели (и игроки) не будут сомневаться в их истинности. В фантастических произведениях перемещение часто выполняется со скоростями больше световой. Чаще всего описываются перемещения кораблей в многомерном или искривленном пространстве. ГИПЕРПРОСТРАНСТВО Четыре основных вида взаимодействий, присутствующих во Вселенной - это электромагнитное, сильное ядерное, слабое ядерное и гравитационное. Пока нет единой теории, описывающей все эти взаимодействия одновременно. Теории, которые пытаются это сделать, чаще всего подразумевают, что Вселенная состоит больше чем из четырех измерений. Пространство из больше чем трех измерений называется гиперпространством. В фантастических произведениях предполагается, что скорость перемещения в гиперпространстве может быть больше световой. На самом деле, даже если гиперпространство существует, это не значит, что мы сможем путешествовать в нем; кроме того, ничто не мешает предельной скорости перемещения через него быть не больше, а меньше скорости света. Но, создавая игры, мы не обращаем на это внимания. В играх путешествие через гиперпространство изображается самыми разными способами. Например, вспомните сериалы Star Wars, Babylon 5 или Andromeda. Игрок примет то, что вы ему предложите, если вы все достаточно красочно и убедительно изобразите. ЧЕРВОТОЧИНЫ Еще один теоретически предсказанный метод путешествий - через червоточины (wormholes). Червоточина - это туннель в гиперпространстве, соединяющий две точки в обычном пространстве. Существование таких туннелей предсказано теорией, которая, увы, описывает и их свойства. Перемещающийся через червоточину объект будет раздавлен сокрушительной гравитацией, которая создала эту червоточину. Но в играх мы обычно не обращаем на это внимания.
Авиация и космические корабли 469 Если вы хотите использовать червоточины в игре, это несложно. Кроме того, есть предположения об особом виде энергии — теневом излучении (ghost radiation), существование которого якобы предсказывают квантовые теории. Согласно этим теориям, данный вид энергии может противодействовать обычным видам энергии, включая гравитационную. Так что если корабль, летящий по червоточине, использует защиту, основанную на теневом излучении, то все будет в порядке. ИСКРИВЛЯЮЩИЕ ДВИГАТЕЛИ, ДВИГАТЕЛИ НА БЕСКОНЕЧНОЙ НЕВЕРОЯТНОСТИ И ПРОЧИЕ КАРИКАТУРНЫЕ УСТРОЙСТВА У некоторых способов космических путешествий вообще нет теоретических основ. Но люди не обращают на это внимания, поскольку эти способы не противоречат нашему повседневному опыту, и их интересно представлять. Пример такого способа - искривляющие двигатели (warp drives) из сериала Star Trek. Предполагается, что эти двигатели искривляют пространство таким образом, что обычные законы физики перестают действовать. При этом можно двигаться и передавать информацию со скоростями, превышающими скорость света. Для этого используется подпространство, которое в играх изображается так же, как гиперпространство. Если в вашей игре пародируются законы физики и фантастические произведения, можно использовать для путешествия методы, не основанные ни на чем, кроме юмора. Пример - серия книг «Путеводитель для путешественников автостопом по галактике» (Hitchhiker's Guide to the Galaxy). В этих книгах персонажи путешествуют на корабле, генерирующем невероятность. Идея заключается в том, что нахождение корабля в какой-то точке становится настолько невероятным, что он перемещается в другую точку. В процессе перемещения может произойти все что угодно - и часто происходит. В этой же серии книг описан корабль, изображающий бистро. Перемещение начинается, если вы начинаете спорить с барменом за стойкой. Этот спор разбалансирует статистику Вселенной и переносит вас в другую точку. Это, конечно, нереалистично, зато довольно весело. Игроки обычно принимают на веру все, что выглядит убедительно и не противоречит здравому смыслу. Но будьте последовательны. Методы, использовавшиеся в первых версиях игры, не должны противоречить методам, которые будут использоваться в будущих версиях. Если все выглядит аккуратно и реалистично, то все будет в порядке. Итоги В этой главе мы рассмотрели методы реализации летательных аппаратов и космических кораблей. Мы разобрались, как изображать их в играх и описывать в коде. Способность путешествовать в космосе и летать добавляет в игры массу интересных возможностей.
Эпилог В этой книге было затронуто множество тем. Мы начали с изучения Windows и программирования под DirectX, с основ математики и геометрии. Продвигаясь дальше, мы прошли путь от шариков, отскакивающих от стен, до принципов полета космических кораблей. Рассмотрев причины замедления течения времени, мы затронули сферу теории относительности. Это очень много для одной книги. Теперь вы, вероятно, сможете создавать игры, в которых физика моделируется настолько же точно, как и в играх, созданных профессионалами. Такое моделирование физики позволит вам изображать реальный мир достаточно убедительно даже для самых требовательных игроков. Из кода, рассмотренного в этой книге, можно извлечь немало полезного. Платформа физического моделирования создавалась в расчете на использование в реальных играх, но подразумевала, что сам движок игры вы напишете с нуля. Именно поэтому в ней довольно просто реализовано взаимодействие с DirectX и Windows. Платформу можно легко расширить, добавив в нее дополнительные возможности работы с DirectX и Windows. Если вы не хотите писать движок с нуля, перенести физическую и математическую части платформы можно практически в любой движок. Это позволит вам приступить к созданию собственно игры гораздо быстрее. Я настоятельно рекомендую вам продолжать изучать физику. Вы уже знаете основы физики и наиболее распространенные термины, используемые в книгах по физике. Это позволит вам приступить к изучению более сложных областей физики. Компьютеры становятся все более производительными с каждым годом, и объемы их памяти тоже растут. Это позволяет им все реалистичнее изображать окружающий мир и моделировать действующие в нем физические законы. Знание этих физических законов делает вас гораздо более ценным работником для компаний, занимающихся созданием компьютерных игр. Однако не стоит забывать, что даже самое лучшее моделирование физики само по себе не сделает вашу игру хорошей. В игру должно быть интересно и приятно играть. Да, физика должна присутствовать в играх. Она делает их более правдоподобными и увлекательными. Но не стоит делать игры настолько реалистичными, что в них будет невозможно играть. Создавая игры, вы можете быть программистом, физиком, художником, музыкантом и, возможно, даже писателем. Получайте от этого удовольствие. Именно ради этого игры и создаются. Если вы не получаете от этого удовольствия, зачем этим заниматься? Есть множество занятий, которые не доставляют удовольствия, и многие из этих занятий гораздо легче, чем создание игр. Создавая игру, вы получаете возможность погрузить игроков во вселенную, целиком созданную вами. Что может быть более захватывающим?
Часть IV Приложения Приложение А Глоссарий 472 Приложение В Краткий обзор языка C++ 475 Приложение С Основы программирования для Windows 489
Приложение А Глоссарий Component Object Model (COM) - модель построения компонентов программ, позволяющая программистам обращаться к функциональности объектов с помощью интерфейсов или методов. Объекты СОМ компонуются динамически в процессе выполнения программы. Hardware Abstraction Layer (HAL) - низкоуровневый API, преобразующий команды DirectX в инструкции, выполняемые аппаратными устройствами. Hardware Emulation Layer (HEL) - низкоуровневый API, эмулирующий возможности, отсутствующие у аппаратных устройств компьютера, на котором выполняется программа. Вектор (vector) - объект, характеризуемый как величиной, так и направлением. Вершинный или вертексный буфер (vertex buffer) - область в оперативной памяти или видеопамяти, в которой хранится список вершин, подлежащих обработке. Виртуальная масса (virtual mass) — масса корабля плюс масса воды, которую он увлекает за собой при движении. Выравнивающий момент (restoring moment) - вращающий момент, заставляющий корабль выравниваться при крене. Гипотенуза (hypotenuse) — самая длинная сторона в прямоугольном треугольнике. Глобальные координаты (world coordinates) - координатная система трехмерной сцены. Единичная матрица (identity matrix или unit matrix) - квадратная матрица, элементы на главной диагонали которой равны 1, а все остальные элементы равны 0. Заставка (splash screen) - экран, отображаемый большинством игр при запуске. Обычно на этом экране изображены название игры, эмблема компании-разработчика и так далее. Инкапсуляция (encapsulation) - методика, используемая в объектно-ориентированном программировании для контроля доступа к данным в объектах программ.
Глоссарий 473 Интерфейс (interface) - набор взаимосвязанных функций. В СОМ такие функции называются методами, доступ к ним осуществляется через СОМ-объекты. Координаты модели (model coordinates) - другое название локальных координат. Коэффициент трения качения (static coefficient of friction) — величина, указывающая степень трения между двумя объектами, катящимися друг по другу. Значение этой величины определяется свойствами объектов. Коэффициент трения скольжения (dynamic coefficient of friction) — величина, указывающая степень трения между двумя объектами, скользящими друг по другу. Значение этой величины определяется свойствами объектов. Крен (roll) - поворот самолета относительно его продольной оси. Локальные координаты (local coordinates) - система координат, в которой определена трехмерная модель, - обычно начало этой системы соответствует центру модели. Метацентр (metacenter) - на корабле - точка пересечения прямой, по которой направлена выталкивающая сила, и плоскости симметрии корабля. Метод (method) - функция класса или СОМ-интерфейса. Неактивная видеостраница (back buffer) - поверхность Direct3D, на которой выполняется рисование. По завершении рисования эта видеостраница становится активной и отображается на экране. Нормальный вектор (normal vector) - вектор, перпендикулярный заданной плоскости. Ортогональная матрица (orthogonal matrix) - матрица, обратную матрицу которой можно найти простым транспонированием. Ортогональный (orthogonal) - перпендикулярный. Перегрузка функций (function overloading) - создание множества функций с одинаковыми именами. Переключение видеостраниц (page flipping) - выбор новой активной видеостраницы из нескольких используемых. При этом содержимое выбранной страницы выводится на экран. Поверхность (surface) — в Direct3D - буфер в памяти, в котором отрисовывается кадр анимации. Подсчет ссылок (reference counting) - метод разделения данных между объектами, основанный на наличии ссылок на эти данные во множестве объектов. Полезная нагрузка (payload) - пассажиры и груз, несомые ракетой или космическим кораблем. Поперечные волны (transverse waves) - волны, в которых частицы движутся в направлении, перпендикулярном направлению распространения волн.
474 Приложение А Прямоугольный треугольник (right triangle) - треугольник, один из углов которого равен 90°. Разрыв (tearing) - состояние, возникающее при переключении видеостраниц во время обновления изображения на мониторе. При этом на мониторе отображается часть содержимого страницы, прежде бывшей активной, и часть содержимого новой активной страницы. Растеризация (rasterization) - вывод трехмерной сцены, прошедшей через конвейер рендеринга. Рыскание (yaw) - поворот самолета в горизонтальной плоскости. Скаляр (scalar) - число, у которого есть только величина, но не направление. Тангаж (pitch) - подъем или опускание носа самолета относительно горизонтальной плоскости. Тексель (texel) - одна точка цвета из текстуры. Эквивалент пикселя. Тензор (tensor) - геометрический объект, компоненты которого в некоторой координатной системе представляют собой квадратную матрицу. Трассировка лучей (ray tracing) - вычисление пути лучей от их источника до глаза наблюдателя. Трение качения (static friction) - трение между двумя объектами, один из которых катится по другому без скольжения. Трение скольжения (dynamic friction) - трение между двумя объектами, скользящими друг по другу. Центр водоизмещения (center of buoyancy) — в корабле — точка, к которой прикладывается выталкивающая сила. Центр тяжести (center of gravity) - точка, к которой прикладывается сила тяжести. То же самое, что и центр масс.
Приложение В Краткий обзор языка C++ Когда язык C++ только появился, многие программисты избегали его использовать, поскольку написанные на нем программы выполнялись недостаточно быстро. Но с тех пор компиляторы C++ прошли долгий путь и теперь они генерируют эффективный быстро выполняющийся код, поэтому большинство современных игр пишутся на C++. Если вы не знаете C++, я не смогу научить вас программировать на нем в рамках этой книги. Единственное, что в моих силах, - дать краткий обзор C++. Хотя в этом обзоре не затрагиваются многие возможности C++, он должен помочь вам разобраться в C++ в достаточной степени, чтобы понимать код из примеров программ, приведенных в этой книге. В основе C++ лежат две базовые концепции. Это функции и объекты. В этом приложении мы кратко их рассмотрим. Кроме того, мы попробуем разобраться с некоторыми вспомогательными возможностями языка C++. Все начинается с функций В колледже я в основном изучал компьютерные науки. Как и все, кто их изучал, я умел писать и проектировать программы. Позже в моей карьере мне пришлось заняться написанием книг. Одна из первых особенностей, которые я заметил, — написание программ и написание книг требуют очень схожих умений. В обеих задачах главное - знать язык, с помощью которого можно передавать логический поток идей. При написании программ каждый оператор в них можно рассматривать как предложение. В каждом предложении есть какой-то глагол. В операторах языка C++ глаголами являются функции. Функции выполняют в программах C++ те же действия, что и глаголы в предложениях человеческих языков. Функция - это блок кода, которому присвоено имя. Этот блок выполняет определенную задачу. Функция main() и функции, вызываемые из нее В языке C++ есть особая функция, которая называется main(). Выполнение программ на языке C++ начинается с первого оператора в функции main (). Это делает функцию main () основной функцией программы. В листинге В.1 приведен пример функции main ().
476 Приложение В Листинг В.1. Пустая функция main() 1 int main() 2 { 3 // Здесь размещается код на C++ 4 5 return @); 6 } Функция main () из листинга В.1 обладает всеми характерными чертами функций C++. Первая такая черта - имя. Имя этой функции - main (). У всех функций также есть списки параметров и типы возвращаемых значений, которые мы рассмотрим немного позже. Операторы между символами { и } (фигурными скобками) выполняют все действия, которые должна выполнять функция. Все операторы в функции всегда размещаются между фигурными скобками. Функция main () вызывается при запуске программы. Эта функция может вызывать другие функции. Вызванные функции в свою очередь могут вызывать еще какие-то функции и так далее. Да, есть ограничение на количество уровней вызовов функций, но это ограничение редко достигается в программах. Функция main () из листинга В. 1 ничего не делает. В ней есть только комментарий и оператор return. Комментарии важны только для людей - компилятор соверигенно не обращает на них внимания. Оператор return мы вскоре рассмотрим. Параметры При вызове функции ей можно передать информацию с помощью списка параметров. Предположим, например, что у меня есть функция UpdateAnimationFrame (), выполняющая прорисовку кадров анимации. Предположим, что она вызывает другую функцию - Calculate- NextPosition(), вычисляющую положение самолета в каждом кадре. Функция UpdateAnimationFrame () может передавать информацию функции CalculateNextPosition() через список параметров функции CalculateNextPosition(). Список параметров размещается в скобках в конце имени функции. Списки параметров могут быть пустыми, а могут и содержать данные. Для каждого параметра задается его тип. Наиболее распространены типы int (целые числа) и float (числа с плавающей запятой). Возвращаемые значения Функции могут передавать информацию функциям, которые их вызвали. Обычно это делается с помощью оператора return. Можно передавать информацию и другими способами, но рассмотрение этих способов
Краткий обзор языка С++ 477 выходит за рамки данного обзора. Чтобы вернуть значение вызывавшей функции, поместите в вызванную функцию оператор return, а за ним в скобках - значение, которое нужно вернуть. Обычно оператор return размещается в конце функции. Встраиваемые функции Встраиваемые функции очень полезны при написании игр. Они выполняются быстрее, чем обычные функции. При вызове обычных функций в выполняемой программе происходит переход из точки вызова функции к самой этой функции. После того, как функция выполнится, происходит обратный переход к точке вызова. Но при вызовах встраиваемых функций таких переходов нет. Компилятор подставляет код встраиваемых функций прямо в места их вызова в программе. При этом программа становится больше по размеру, но выполняется быстрее за счет отсутствия переходов. Не все функции стоит превращать во встраиваемые. Встраиваемыми лучше всего делать маленькие функции, в которых нет вызовов других функций. Если в функции больше десятка строк кода, лучше не делать ее встраиваемой. В листинге В.2 приведен пример определения встраиваемой функции. Листинг В.2. Встраиваемая функция 1 inline scalar DegreesToRadians(float degrees) 2 < 3 return (degrees * D3DX_PI / 180); 4 } Классы и объектно-ориентированное программирование Язык C++ позволяет вам создавать собственные типы данных. С этими типами данных можно связывать функции. Эти функции определяют набор действий, которые можно выполнять над данными соответствующего типа. Создание типов и определение наборов выполняемых над ними операций выполняется с помощью классов. В классе определяется набор элементов данных и связанных с ними функций (эти функции называются методами класса). В каждом элементе данных содержится одно значение, тип которого определяется типом этого элемента данных. Обычно элементы данных класса определяются таким образом, что к ним могут обращаться только методы данного класса. Такой подход называется инкапсуляцией (encapsulation). Ограничивая
478 Приложение В доступ к элементам данных, можно защитить их от неправильного обращения к ним из программ. Это помогает уменьшить количество ошибок в программах. Пример определения класса приведен в листинге В.З. Листинг В.З. Определение класса 1 class point 2 { 3 private: 4 int x, у; 5 6 public: 7 point(); 8 point(int xValue, int yValue); 9 10 void X(int Value); 11 int X(void); 12 13 void Y(int yValue); 14 int Y(void); 15 }; Все определения классов в языке C++ начинаются с ключевого слова class. За ним идет имя класса. Внутри фигурных скобок, следующих за именем класса, перечисляются элементы данных класса и прототипы его методов. Прототипы — это краткие описания методов. В них указываются имена, типы возвращаемых значений и списки параметров этих методов. Класс в листинге В.З называется point. В классе point есть два элемента данных: х и у. Это элементы данных типа int, и в них могут храниться целые числа. Поскольку перед их объявлением стоит ключевое слово private, к ним смогут обращаться только методы класса point. Прототипы этих методов перечислены в строках 7-14 листинга В.З. В классе point есть шесть методов. Два из них называются point{). Это особые методы, которые называются конструкторами (constructor). Они используются в программах для инициализации объектов при их создании. Методы в строках 10-11 листинга В.З называются X (). В C++ разрешено задавать одно имя нескольким методам класса, и часто программисты именно так и делают. Компилятор различает функции с одинаковыми именами по их спискам параметров. Этот прием называется перегрузкой функций (function overloading). Заметьте, что метод X () из строки 10 листинга В.З принимает параметр типа int, а метод из строки 11 не принимает никаких параметров. Кроме того, метод из строки 10 не возвращает никаких значений, а метод из строки 11 возвращает значение типа int. Эти методы, соответственно, считывают и задают значение элемента данных х. В листинге В.4 приведен код этих методов.
Краткий обзор языка С++ 479 Листинг В.4. Два метода Х() класса point 1 inline void point::X(int xValue) 2 { 3 x = xValue; 4 } 5 6 inline int point::X(void) 7 { 8 return (x) ; 9 } Возможно, вам интересно, как программа определяет, какой из двух методов X () нужно вызывать в каждом конкретном случае. Программа определяет это по спискам параметров и типам возвращаемых значений. Если при вызове метода X () ему передается параметр типа int, то программа вызывает первый метод (строки 1-4 листинга В.4), а если при вызове не передаются никакие параметры, то вызывается второй метод (строки 6-9 листинга В.4). Пример использования объектов класса содержится в функции main () из листинга В.5. Листинг В.5. Использование объектов класса point 1 int main() 2 { 3 point р; 4 5 р.Х(Ю); 6 7 int anXValue = p.X(); 8 9 return @) ; 10 > Функция main () из листинга В.5 объявляет переменную р типа point. В этой переменной будут храниться два целочисленных значения, переменные для которых объявлены в строке 4 листинга В.З. Чтобы задать значение элемента х объекта р, функция main () вызывает метод X () класса point. Если метод объявлен как public, то его можно вызывать из любой функции программы. При вызове метода X () в строке 5 ему в качестве аргумента передается значение 10. Соответственно, метод присваивает значение 10 элементу данных х объекта р. В строке 7 функция main () вызывает другой метод Х(), чтобы прочитать значение элемента данных х объекта р. Прочитанное и возвращенное методом значение присваивается переменной anXValue типа int.
480 Приложение В Пространства имен При написании сложных программ, в частности, игр, часто используются библиотеки, созданные разными компаниями. Иногда в этих библиотеках встречаются функции или классы с совпадающими именами. В C++ есть удобный метод различения таких функций и классов. Этот метод основан на применении пространств имен (namespaces). Пространство имен позволяет объединять функции и классы, содержащиеся в библиотеке. Для обращения к членам пространства имен используется операция расширения области видимости. Например, в этой книге мы создавали платформу физического моделирования, которая представляет собой библиотеку. Ко всем функциям и классам этой библиотеки можно обращаться через пространство имен библиотеки. Это пространство имен называется pmframework. Пространство имен объявляется с помощью ключевого слова namespace. Например, все в пространстве имен pmframework объявляется внутри такого оператора: namespace pmframework { // Здесь перечисляются классы, функции и другие элементы } Все классы, функции и другие элементы пространства имен перечисляются внутри фигурных скобок после имени этого пространства имен. Чтобы сообщить программе об использовании пространства имен, нужно поместить в начале ее срр-файла оператор using, например, такой: using pmframework; Если такой оператор вставить в начало срр-файла, то в этом файле можно обращаться ко всем элементам пространства имен pmframework просто по их именам. Предположим, что в вашей программе используются библиотека физического моделирования и библиотека для работы с графикой. В библиотеке физического моделирования содержится класс vector_3d. Представьте себе, что в библиотеке для работы с графикой тоже есть такой класс. Как сообщить программе, какой из двух классов вам нужен? С помощью оператора расширения области видимости. Этот оператор всегда позволяет различить две функции или два класса с одинаковыми именами. Если эти функции или классы находятся в разных пространствах имен, различить их очень просто, например: pmframework::vector_3d vectorVariable; a_graphics_lib::vector_3d anotherVectorVariable;
Краткий обзор языка C++ 481 В каждом из этих операторов объявляется переменная типа vec- tor_3d. Но в первом операторе подразумевается класс из пространства имен pmf ramework, то есть из библиотеки физического моделирования, а во втором - из пространства имен a_graphics_lib, то есть из библиотеки для работы с графикой. Хотя имена классов и совпадают, это разные классы из разных пространств имен. Наследование Наследование - это возможность, присутствующая во всех объектно-ориентированных языках программирования. Она позволяет создавать код, который легко можно повторно использовать. С помощью наследования можно расширять возможности существующих классов без необходимости переписывать их заново или изменять уже написанный код. Классы, от которых ведется наследование, называются базовыми (base) или родительскими (parent) классами. Классы-наследники называются производными (derived) классами или классами-потомками. Классы-потомки наследуют все элементы данных и методы родительских классов. Собственно говоря, в большинстве реализаций компиляторов классы-потомки содержат в себе копии родительских классов. Чтобы унаследовать класс, нужно указать имя родительского класса в определении класса-потомка. Пример наследования класса приведен в листинге В.6. В листинге В.6 определен класс point_3d. Этот класс является потомком класса point, который мы рассматривали выше. Наследование указано в строке 1, после двоеточия. Заметьте, что в классе point_3d класс point унаследован как public. Это значит, что если в программе создается объект класса point_3d, то для этого объекта можно будет вызывать методы класса point. Как это сделать, вы узнаете немного позже. В классе point_3d объявлен только один элемент данных. Поскольку он является потомком класса point, в нем содержатся и все элементы данных класса point. Поэтому на самом деле в классе point_3d три элемента данных. Кроме элементов данных, класс point_3d унаследовал у класса point и все его методы. Поэтому класс point_3d содержит не только методы, перечисленные в листинге В.6, но и методы из листинга В.З. Конструкторы класса point_3d немного отличаются от обычных конструкторов из-за использования наследования. В строках 14-17 листинга В.6 показан конструктор класса В.6, не принимающий параметров. Это так называемый конструктор по умолчанию. Он вызывает такой же конструктор класса point, который проинициализирует элементы данных х и у. Конструктор класса point_3d проинициализирует элемент z.
482 Приложение В Кроме того, в классе point_3d есть конструктор с тремя параметрами, показанный в строках 19-25 листинга В.6. Этот конструктор вызывает конструктор с двумя параметрами класса point. Он передает конструктору класса point свои первые два параметра, чтобы проинициализировать их значениями элементы данных х и у. Третий параметр используется для инициализации элемента z. Листинг В.6. Использование наследования 1 class point_3d : public point 2 { 3 private: 4 int z; 5 6 public: 7 point_3d(); 8 point_3d(int xValue, int yValue, int zValue); 9 10 void Z(int zValue); 11 int Z(void); 12 }; 13 14 inline point_3d::point_3d : point() 15 { 16 z = 0; 17 } 18 19 inline point_3d::point_3d( 20 int xValue, 21 int yValue, 22 int zValue) : point(xValue, yValue) 23 { 24 z = zValue; 25 } 26 27 inline void point_3d::Z(int zValue) 28 { 29 z = zValue; 30 ) 31 32 inline int point_3d::Z(void) 33 { 34 return (z); 35 } Класс point_3d можно использовать точно так же, как и любой другой класс. Пример его использования - короткая программа из листинга В.7.
Краткий обзор языка С++ 483 Листинг В.7. Использование класса point_3d 1 int main() 2 { 3 point р; 4 5 р.Х(Ю); 6 7 int anXValue = p.X(); 8 9 point_3d p2; 10 p2.XA0); 11 p2.YB0); 12 p2.ZC0); 13 14 return @); 15 } Эта программа показывает, что в одной и той же функции можно использовать и родительский класс, и класс-потомок. В функции main() объявлены переменные типов point и point_3d. В строках 10-11 программа использует переменную point_3d для вызова методов X () и У () класса point. В строке 12 точно таким же образом вызывается метод Z () класса point_3d. Переопределение функций Функция в классе-потомке может иметь такое же имя, как и функция в родительском классе. Если мы создаем такие функции, то говорим, что мы переопределяем функции родительского класса. Например, предположим, что мы создали класс саг, в котором есть метод Update (). Предположим, что у нас также есть класс sports_car, в котором тоже есть метод Update (). Класс sports_car является потомком класса саг. Если метод Update () вызывается для переменной типа sports_car, то программа будет вызывать метод класса sports_car. При этом метод Update () класса саг вызываться не будет. Если класс-потомок должен расширять возможности родительского класса, то в нем часто переопределяются методы родительского класса. При этом в методах класса-потомка часто вызываются переопределяемые методы родительского класса. Например, метод Update () класса sports_ car может содержать вызов метода Update () класса саг и выполнять какие-то операции, которые не выполняются в методе класса саг. Если вы хотите вызвать переопределенный метод родительского класса для объекта производного класса, то это нужно делать с помощью операции расширения области видимости.
484 Приложение В Виртуальные методы Особая разновидность методов, называемая виртуальными методами (virtual methods), основана на работе с указателями. Рассмотрение указателей выходит за рамки данного обзора, однако стоит запомнить, что если вы собираетесь использовать класс как родительский для создания других классов, методы этого класса нужно сделать виртуальными. Листинг В.8. Класс с виртуальными методами 1 class rigidMbody : public point_mass 2 { 3 private: 4 // Свойства вращательного движения 5 angle_set_3d currentOrientation; 6 vector_3d angularVelocity; 7 vector_3d angularAcceleration; 8 vector_3d rotational_inertia; 9 vector_3d torque; 10 11 public: 12 rigidjbody(); 13 14 virtual void CurrentOrientation( 15 angle_set_3d newOrientation); 16 virtual angle_set_3d CurrentOrientation(void); 17 18 virtual void AngularVelocity( 19 vector_3d newAngularVelocity); 20 virtual vector_3d AngularVelocity(void); 21 22 virtual void AngularAcceleration( 23 vector_3d newAngularAcceleration); 24 virtual vector_3d AngularAcceleration(void); 25 26 virtual void Rotationallnertia( 27 vector_3d inertiaValue); 28 virtual vector_3d Rotationallnertia(void); 29 30 virtual void Torque( 31 vector_3d torqueValue); 32 virtual vector_3d Torque(void); 33 34 virtual bool Update( 35 scalar changelnTime); 36 };
Краткий обзор языка С++ 485 Класс rigid_body из листинга В.8 унаследован от класса point_mass. От класса rigid_body унаследованы другие классы. Поскольку класс rigid_body является родительским классом, его методы должны быть виртуальными. Исключения В высокоуровневых языках, например, в C++, есть специальный механизм обработки ошибок, называемый обработкой исключений (exception handling). Если использующая его программа сталкивается с ошибкой при выполнении, она генерирует исключение, которое другая часть программы может обработать, чтобы попытаться избежать последствий ошибки. Исключения должны использоваться только в особых случаях, например, при возникновении ошибок, из-за которых невозможно продолжать выполнять программу. Когда функция генерирует исключение, оно может быть перехвачено в той же функции, или в функции, из которой она вызвана, или в любой функции, лежащей еще выше в последовательности вызовов. Обычно при генерации исключения программа создает объект, содержащий информацию об исключении. Пример определения класса таких объектов приведен в листинге В.9. Листинг В.9. Класс исключения 1 class pmlib_error 2 { 3 private: 4 std::string errorString; 5 6 public: 7 pmlib_error(std::string errorMessage); 8 std::string ErrorMessage(void); 9 }j Класс из листинга В. 9 содержит строку, в которой можно хранить сообщение об ошибке. Программа записывает в эту строку сообщение об ошибке, создавая объект класса pmlib_error. Затем этот объект можно использовать при генерации исключения. В листинге В. 10 показано, как это делается. Для нашей дискуссии неважно, что именно делает функция из листинга В. 10. Нас интересует только проверка наличия ошибки, которая выполняется в строке 4. Если обнаруживается ошибка, то в строках 6-7 создается переменная типа pmlib_error, и в нее помещается сообщение об ошибке. Затем в строке 8 оператором throw генерируется исключение.
486 Приложение В Оно не обрабатывается в этой функции, но может быть обработано любой функцией, вызывающей эту, например, функцией, приведенной в листинге В. 11. Листинг В. 10. Генерация исключения 1 inline matrix2x2 matrix2x2::Inverse() 2 { 3 scalar determinant = Determinant(); 4 if (determinant == 0.0) 5 { 6 pmlib_error theError( 7 "Can't invert a matrix that has a determinant of 0."); 8 throw theError; 9 } 10 11 return( 12 matrix2x2( 13 elements[1][1]/determinant,-elements[0][1]/determinant, 14 elements[1][0]/determinant,elements[0][0]/ determinant)); 15) Листинг В.11. Обработка исключений 1 matrix2x2 aMatrix; 2 try 3 { 4 aMatrix.Inverse() ; 5 } 6 7 catch (pmlib_error libError) 8 { 9 cout « libError.errorMessage(); 10 ) Блок кода в листинге В. 11 объявляет объект класса matr 1x2x2. Это класс, к которому принадлежит метод Inverse () из листинга В. 10. В строке 4 листинга В.11 этот объект используется для вызова метода Inverse (). Если метод Inverse () генерирует исключение, оно перехватывается в строке 7. При этом выполняется блок catch, который обрабатывает исключение. В нашем случае обработка исключения сводится к выводу сообщения об ошибке.
Краткий обзор языка С++ 487 Другие способы создания новых типов В языке C++ есть и другие способы создания новых типов, помимо механизма классов. В коде этой книги чаще всего использовались структуры, перечисляемые типы и операторы typedef. Структуры Структуры - это почти то же самое, что и классы. Наиболее существенное отличие - то, что элементы классов по умолчанию объявляются как private, а элементы структур - как public. В листинге В. 12 иллюстрируется эта разница. Листинг В. 12. Структуры и классы 1 class point 2 { 3 int х, у; 4 } 5 6 struct point 7 { 8 int x, у; 9) Ни в структуре, ни в классе из листинга В. 12 нет ключевых слов public или private. Поэтому все элементы класса объявляются как private, а все элементы структуры - как public. Как и в классах, в структурах можно размещать элементы данных, методы и другие составляющие. Перечисляемые типы Перечисляемые типы (enumerated type) - это еще один способ создания типов и связывания с ними определенных значений. Пример перечисляемого типа приведен в листинге В. 13. Имя типа, определенного в листинге В.13 - cloth_constants. Это имя можно использовать при объявлении переменных. Объявляя переменную, можно присвоить значения элементам, перечисленным в строках 3-14. Эти элементы на самом деле представляют собой целые числа. Можно задать новые значения этим элементам или оставить значения, заданные по умолчанию. В примере делается и то, и другое.
488 Приложение В Листинг В. 13. Перечисляемый тип 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 enum cloth constants { PARTICLES PER SQUARE=4, TOP LEFT PARTICLE=0, TOP RIGHT PARTICLE, BOTTOM LEFT PARTICLE, BOTTOM RIGHT PARTICLE, TOP SPRING = 0, BOTTOM SPRING, RIGHT_SPRING, LEFT SPRING, TOP_RIGHT_TO_BOTTOM_LEFT_SPRING, TOP_LEFT_TO_BOTTOM_RIGHT_SPRING, SPRINGS PER SQUARE=6, }; В строках З и 4 соответствующим элементам присваиваются значения 4 и 0. В строках 5-7 константам в явном виде не присваиваются никакие значения. Вместо этого компилятор увеличивает на 1 значение из строки 4 и присваивает результат (число 1) константе в строке 5. Точно так же обрабатываются константы в строках 6 и 7 - они получают значения соответственно 2 и 3. Операторы typedef Оператор typedef не создает нового типа. Он просто позволяет использовать новое имя для обозначения уже существующего типа. Например, в платформе физического моделирования есть оператор typedef point_mass_base invisible_point_mass; Этот оператор сообщает программе, что имя invisible_point_mass обозначает тип point_mass_base. После того, как этот оператор появится в файле, тип point_mass_base можно обозначать обоими именами — invisible_point_mass будет просто псевдонимом для point_mass_base. Использование оператора typedef не запрещает использование имени, для которого этот оператор создает псевдоним.
Приложение С Основы программирования для Windows У большинства людей на компьютерах установлена та или иная версия Windows. Чтобы писать игры для разных версий Windows, не обязательно быть экспертом по этой операционной системе. Достаточно знать несколько базовых концепций, например: О как создавать окно в функции WinMain (); Q как определять процедуру обработки сообщений Windows; Q как использовать ресурсы Windows, например, значки, курсоры и так далее. Добро пожаловать в WinMain() В каждой программе для Windows должна присутствовать функция Win- Main (). Эта функция содержит точку входа в Windows-программу, то есть выполнение программы начинается с выполнения функции WinMain (). Она выполняет практически те же задачи, что и функция main () в большинстве обычных программ на С и C++. Функция WinMain () создает и отображает рабочее окно программы. Чтобы сделать это, она должна выполнить следующие действия: 1. Определить класс окна. 2. Зарегистрировать этот класс, 3. Создать окно зарегистрированного класса. 4. Вывести созданное окно на экран. 5. Обрабатывать сообщения в цикле обработки сообщений.
490 Приложение С Кроме функции WinMain (), в каждой программе для Windows должна присутствовать процедура обработки сообщений. Это специальная функция, принимающая сообщения, которые обрабатываются в цикле обработки сообщений функции WinMain (). Написание функции WinMain() Как уже говорилось выше, во всех программах для Windows должна присутствовать функция WinMain (). В файл, в котором находится эта функция, должен быть включен стандартный заголовочный файл Windows. h. Это дает программе доступ к типам, функциям и другим элементам, которые ей нужны для превращения в программу для Windows. У функции WinMain () есть набор параметров, заданных операционной системой Windows. В листинге С.1 показана пустая функция WinMain () со списком параметров. Листинг С. 1. Пустая функция WinMain() 1 INT WINAPI WinMain( 2 HINSTANCE hlnstance, 3 HINSTANCE hPrevInstance, 4 LPSTR lpCmdLine, 5 INT nCmdShow) 6 { 7 return @); 8 } Первые два параметра функции WinMain () представляют собой дескрипторы. Дескриптор - это нечто вроде идентификатора. Первый из двух дескрипторов обозначает текущий экземпляр программы. Второй может обозначать предыдущий экземпляр. Эти дескрипторы можно использовать, чтобы предотвратить запуск нескольких копий игры сразу. Обычно это весьма полезная возможность - для большинства игр не имеет смысла запускать несколько их экземпляров сразу. Второй параметр функции WinMain () всегда равен NULL для приложений Win32. Чтобы определить, запущены ли другие копии вашей игры, воспользуйтесь объектом Windows под названием мьютекс (mutex, сокращение от mutual exception - взаимное исключение). Чтобы создать этот объект, вызовите функцию CreateMutex () - это одна из функций API Win32. Создавая мьютекс, задайте ему уникальное имя. Оно не должно использоваться никакой другой программой. После выполнения функции CreateMutex () вызовите функцию GetLastError () — это еще одна функция API Win32. Если функция GetLastError () возвращает значение ERROR_ALREADY_EXISTS, значит, уже запущена другая копия вашей программы. В этом случае вызовите функцию MessageBox (), чтобы вывести сообщение об ошибке и завершите выполнение программы.
Основы программирования для Windows 491 Определение класса окна Класс окна - это не то же самое, что класс C++. Класс окна сообщает Windows, какой тип окна вы хотите создать для своей программы. Сейчас для определения класса окна обычно используется структура WNDCLASSEX, a раньше для этого использовалась структура WNDCLASS, которая сейчас считается устаревшей согласно рекомендациям Microsoft. В структуре WNDCLASSEX есть множество элементов, описывающих тип создаваемого окна. Однако в играх используются только некоторые из этих элементов. В листинге С.2 приведена, вероятно, простейшая разновидность класса окна. Листинг С.2. Определение простого класса окна 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 WNDCLASSEX myWindowClass = { }; sizeof(WNDCLASSEX), CS CLASSDC, MsgProc, OL, OL, GetModuleHandle(NULL), NULL, NULL, NULL, NULL, "MyWindowClass", NULL Первому элементу структуры WNDCLASSEX в листинге С.2 присваивается значение sizeof (WNDCLASSEX). Этому -элементу должно присваиваться такое значение - в противном случае программа не сможет зарегистрировать класс окна. Второй элемент структуры WNDCLASSEX определяет стиль окна. В строке 4 листинга С.2 показан стиль, чаще вс^го используемый для окон игр. Этот стиль позволяет множеству потоков с легкостью обращаться к одному и тому же окну. Кроме этого стиля, часто используются стили CS_HREDRAW и CS_VREDRAW, при использовании которых окно перерисовывается в случае изменения его размеров. Эти стили можно объединять с помощью битового оператора ИЛИ, например; CS_CLASSDC | CS_HREDRAW | CS_VREDRAW В следующем элементе структуры WNDCLA.SSEX должен содержаться адрес процедуры обработки сообщений. Это функция программы, реагирующая на все сообщения Windows. За дополнительной информацией о
492 Приложение С процедурах обработки сообщений обращайтесь к теме WindowProc в документации по платформе Microsoft Win32. Можно выделить в классе окна дополнительные объемы для хранения данных. Это нечасто используемая возможность, поэтому элементы из строк 6 и 7 листинга С.2 обычно инициализируются нулями. В строке 8 инициализируется шестой элемент структуры WNDCLASSEX, содержащий дескриптор модуля, в котором определен класс окна. Чтобы получить этот дескриптор, программа вызывает функцию Win32 GetMo- duleHandle(). В строке 9 элементу hlcon структуры WNDCLASSEX присваивается значение NULL. Обычно в реальных программах в этом элементе хранится идентификатор ресурса - значка, отображающегося при минимизации окна программы. Я настоятельно рекомендую вам создать такой значок и поместить в элемент структуры идентификатор ресурса этого значка. Ресурсы вроде значков и курсоров мыши можно создавать в Visual Studio. Хотя следующему элементу структуры WNDCLASSEX в листинге С.2 тоже присвоено значение NULL, во многих играх ему присваиваются другие значения. Этот элемент hCursor содержит дескриптор ресурса курсора мыши, используемого в программе. Если ваша игра - стрелялка с видом от первого лица или что-то похожее, то этому элементу можно присваивать значение NULL, как сделано в строке 10. Это покажет Windows, что ваша программа не использует курсор. Но если курсор нужен, то элементу hCursor нужно присвоить другое значение. Чтобы задать определенный цвет фона окна, программа должна сообщить Windows, что нужно использовать кисть (brush). Игры, основанные на DirectX, обычно не используют кисти — все их окна отрисовывает DirectX. Поэтому в таких играх следующему элементу структуры WNDCLASSEX обычно присваивается значение NULL. Пользовательские интерфейсы игр часто радикально отличаются от интерфейсов обычных программ Windows. Поэтому следующий элемент структуры WNDCLASSEX в играх используется далеко не всегда. Этот элемент называется IpszMenuName. Как видно из его имени, в этом элементе содержится идентификатор ресурса для основного меню окна. Чаще всего в играх этому элементу присваивается значение NULL. Игра должна задать уникальный идентификатор каждому классу окна, который она зарегистрирует в Windows. Как видно из строки 13 листинга С.2, этот идентификатор представляет собой строку. Эта строка может быть любой, при условии, что она уникальна. В строке 13 используется строка "MyWindowClass", но в играх лучше использовать что-то более очевидное, например, "<Ha3Bai»ie_iirpbi>_WindowClass", где <название_игры> - это название вашей игры (разумеется, латинскими буквами). В последнем элементе структуры WNDCLASSEX содержится дескриптор ресурса для уменьшенной версии значка, указанного в элементе hlcon. Игры чаще всего присваивают этому элементу значение NULL.
Основы программирования для Windows 493 Регистрация класса окна Определив класс окна, его нужно зарегистрировать в Windows. Это делается с помощью функции Win32 RegisterClassEx (). Просто передайте этой функции адрес структуры WNDCLASSEX, например: RegisterClassEx(SmyWindowClass); Игра должна проверить значение, возвращаемое функцией RegisterClassEx (), чтобы убедиться, что класс окна успешно зарегистрирован. Если класс зарегистрировать не удалось, игра должна вывести сообщение об ошибке и завершиться. Создание окна После того, как класс окна создан и зарегистрирован, его можно использовать для создания окна. Программа создает окно, вызывая функцию Win32 CreateWindow(). Листинг С.З. Вызов функции Win32 CreateWindow() 1 // Создаем окно приложения 2 hWnd = CreateWindow( 3 "MyWindowClass", 4 (LPCSTR)"Window Title", 5 WS_OVERLAPPEDWINDOW, 6 CW_USEDEFAULT, 7 CW_USEDEFAULT, 8 CW_USEDEFAULT, 9 CW_USEDEFAULT, 10 GetDesktopWindow(), 11 NULL, 12 myWindowClass.hlnstance, 13 NULL) ; Вызов функции CreateWindow () в листинге С.З присваивает ее возвращаемое значение переменной hWnd. Это переменная типа HWND, стандартного типа Windows, который доступен в программах, включающих заголовочный файл Windows. h. Первый параметр функции CreateWindow () - это имя класса окна, который зарегистрировала ваша программа. Второй параметр - это заголовок окна. Эти два параметра показаны в строках 3-4 листинга С.З. Далее задается стиль окна программы. Если вы не хотите, чтобы она работала в полноэкранном режиме, задайте стиль WS_OVERLAPPEDWINDOW, как в строке 5 листинга С.З. В документации по функции CreateWindow () перечислено множество разных стилей. Многие из этих стилей
494 Приложение С можно сочетать друг с другом с помощью битового оператора ИЛИ. Наиболее распространенные сочетания перечислены в таблице С.1. Таблица С.1. Часто использующиеся сочетания флагов стиля окна Флаги Описание WS_OVERLAPPEDWINDOW WS VISIBLE WS POPUP | WS VISIBLE WS_POPOP | WS_VISIBLE | WS MAXIMIZE Стандартное окно Windows, которое можно передвигать, уменьшать / увеличивать и так далее. Оно станет видимым после завершения выполнения функции CreateWindow (). Вызывать функцию ShowWindowO ненужно Часто используемый стиль в играх, работающих в полноэкранном режиме. У окна нет границы, его размер нельзя изменять, и оно изначально видимо Еще один часто используемый стиль в играх, работающих в полноэкранном режиме. У окна нет границы, его размер нельзя изменять, оно изначально видимо и развернуто на весь экран Следующие два параметра задают координаты верхнего левого угла окна. Если вы хотите, чтобы игра работала в полноэкранном режиме, задайте для этих параметров нули в качестве значений. Если игра должна работать в оконном режиме, используйте значения CW_USEDEFAULT. Это позволит системе Windows вывести окно на экран согласно ее настройкам по умолчанию. В строках 8-9 листинга С.З задаются высота и ширина создаваемого функцией CreateWindow () окна. Если ваша игра выполняется в оконном режиме, то можете задавать для этих параметров нужные вам значения. Большинство пользователей используют экраны с разрешением не меньше 800 х 600 пикселей, так что можно свободно задавать размеры окна порядка 500 X 500 пикселей. Кроме того, можно использовать предоставляемые Windows значения по умолчанию, задав для этих параметров значения CW_USEDEFAULT. С другой стороны, если игра должна работать в полноэкранном режиме, дважды вызовите функцию GetSystemMetrics О • При первом вызове передайте ей в качестве аргумента значение SM_CXFULLSCREEN. При втором передайте значение SM_CYFULLSCREEN. Эти вызовы вернут, соответственно, высоту и ширину экрана в пикселях. Можно передать полученные значения функции CreateWindow (), чтобы размеры окна соответствовали размеру экрана. В строке 10 листинга С.З показан параметр, представляющий собой дескриптор родительского окна для создаваемого окна программы. Если
Основы программирования для Windows 495 вы создаете диалоговое окно или всплывающее окно сообщения, передайте в этом параметре дескриптор родительского окна. Для основного окна игры или полноэкранного окна нужно передавать значение, возвращаемое функцией Win32 GetDesktopWindow(). Если в окне используется меню, передайте дескриптор соответствующего ресурса в качестве значения следующего параметра функции CreateWindow (). Если меню не используется (как в большинстве игр), передайте значение NULL, как в строке 11 листинга С.З. Если вы разрабатываете игру для версий Windows, предшествовавших Windows NT, нужно передать функции CreateWindow () дескриптор из структуры, которую программа передавала функции RegisterClas- sEx(), как показано в строке 12 листинга С.З. Если ваша игра предназначена для запуска под Windows NT, Windows 2000 или Windows XP, этому параметру можно присвоить значение NULL. Последний параметр функции CreateWindow () используется для передачи информации процедуре обработки сообщений при обработке сообщения WM_CREATE (мы вскоре рассмотрим эту процедуру). В большинстве игр этому параметру присваивается значение NULL. Отображение окна При успешном выполнении функция CreateWindow () возвращает дескриптор созданного окна. Этот дескриптор позволяет игре отображать окно, например: ShowWindow(hWnd, SW_SHOWDEFAULT); UpdateWindow(hWnd); Вызов первой из этих функций отображает окно на экране, а вызов второй посылает окну сообщение о необходимости обновить его содержимое. Обратите внимание на второй аргумент функции ShowWindow () - это константа SW_SHOWDEFAULT. Она сообщает функции, что окно нужно вывести в его состоянии по умолчанию. Состоянием по умолчанию может быть развернутое на весь экран окно, окно в обычном режиме и окно, свернутое в панели задач. Кроме SW_SHOWDEFAULT часто также используются константы SW_SHOWNORMAL и SW_SHOWMAXIMIZED. Обработка сообщений Windows Windows реагирует на сообщения вроде нажатий на клавиши мыши, посылая сообщения вашей программе. Программа должна предоставлять возможность обработки этих сообщений. Обработка выполняется в цикле обработки сообщений (message processing loop), который иногда называют просто циклом сообщений. В листинге С.4 приведен цикл сообщений, используемый в платформе физического моделирования.
496 Приложение С Листинг С.4. Цикл сообщений платформы физического моделирования 1 MSG msg; 2 ZeroMemory(Smsg,sizeof(msg) ) ; 3 while(msg.message!=WM_QUIT) 4 { 5 if(PeekMessage(Smsg,NULL,OU,0И,PM_REMOVE)) 6 { 7 TranslateMessage(Smsg); 8 DispatchMessage(&msg); 9 } 10 else 11 { 12 if (theApp.theGame->ProcessInput()==false) 13 { 14 pmlib_error fatalError(PMERROR_FATAL_INPUT_ERROR); 15 throw fatalError; 16 > 17 18 if (theApp.theGame->UpdateFrame()==false) 19 { 20 pmlib_error fatalError(PMERROR_FATAL_FRAME_ERROR); 21 throw fatalError; 22 } 23 theApp.Render() ; 24 } 25 } Windows передает программе сообщения в структуре MSG. MSG - это стандартный тип, определенный в заголовочном файле Windows. h. Идентификатор каждого сообщения — это элемент message структуры MSG. Цикл сообщений должен выполняться до тех пор, пока программа не получит сообщение WM_QUIT. У программы есть очередь сообщений, к которой можно обращаться, вызывая функцию PeekMessage (). Это делается в строке 5 листинга С.4. Если в очереди есть сообщения, цикл сообщений должен вызвать функцию TranslateMessage (), чтобы убедиться, что сообщение представлено в формате, который программа может обработать. Далее программа должна послать сообщение процедуре обработки сообщений, рассмотренной ниже. Процедура обработки сообщений Стандартные сообщения Windows должны обрабатываться процедурой обработки сообщений. В листинге С.5 показана такая простейшая процедура.
Основы программирования для Windows 497 Листинг С.5. Простейшая процедура обработки сообщений 1 LRESULT WINAPI MsgProc( 2 HWND hWnd, 3 UINT msg, 4 WPARAM wParam, 5 LPARAM lParam) 6 { 7 switch(msg) 8 { 9 case WM_DESTROY: 10 PostQuitMessage(O); 11 return @); 12 break; 13 } 14 return DefWindowProc(hWnd, msg, wParam, lParam); 15 } Эта процедура обработки сообщений реагирует только на сообщение WM_DESTROY. Программа получает такое сообщение, если пользователь сделал что-то, чтобы сообщить программе, что она должна завершиться. Все остальные сообщения данная процедура передает для обработки Windows, вызывая функцию DefWindowProc () в строке 14. В большинстве игр не требуются более замысловатые процедуры обработки сообщений - большая часть поступающей в них информации обрабатывается Directlnput, а не процедурами обработки сообщений Windows.
Только в совершенстве овладев мастерством моделирования физических законов, можно добиться исключительно высокого уровня реалистичности компьютерных игр. В этой книге помимо теоретических основ и практических методов вы найдете полезные рекомендации по приложению обретенного мастерства к реальным задачам, познакомитесь с математическими основами физического моделирования и узнаете, как получить реалистичную картину движения и столкновения объектов в играх. Завершают книгу примеры моделирования движения различных транспортных средств, в том числе автомобилей и летательных аппаратов. Рассматриваются: ЗО-моделирование с помощью DirectX® Математический инструментарий физики и ЗР-программирования 2D- и ЗО-преобразования и рендеринг ЗО-объекты, движения и столкновения Сила тяжести и метательные снаряды Автомобили, транспорт на воздушной подушке, корабли и лодки На компакт-диске Microsoft® DirectX 9 SDK MilkShape 3D CrystalSpace™ 3D Инсталляция Torque Engine Исходный код примеров из книги Об авторе Дэвид Конгер - программист с 20-летним стажем. Он участвовал в написании игр для DOS, многопользовательских Интернет-игр, встроенных программ для графических контроллеров в военной авиации и коммерческих приложений. В течение четырех лет читал курсы по computer science и программированию в колледже. Д. Конгер - автор документации Microsoft, ряда книг по С, C++, С# и программированию для платформы .NET, а также учебника по микрокомпьютерам. ISBN 5-94774-317-5 9^785 947И74З 173