/
Author: Конгер Д.
Tags: физика связь компьютеров сети эвм вычислительные сети компьютерные системы и сети программирование
ISBN: 5-94774-317-5
Year: 2007
Text
ПРОГРАММИСТУ йо йэкщр ФИЗИКА
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 строк. В играх вам придется использовать куда большие сетки с ячейками гораздо меньших размеров. Можно добавить в ткань затухание движения по кв