Физика для разработчиков компьютерных игр 5947743175

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

175 50 7MB

Russian Pages 498 Year 2007

Report DMCA / Copyright

DOWNLOAD PDF FILE

Table of contents :
Краткое оглавление......Page 5
Введение......Page 8
Часть I. Физика, математика и программирование игр......Page 11
Глава 1. Физика в играх......Page 12
Глава 2. Имитация ЗD-графики с помощью DirectX......Page 19
Глава 3. Математические инструменты......Page 51
Глава 4. 2D-преобразования и рендеринг......Page 96
Глава 5. ЗD-преобразования и рендеринг......Page 119
Глава 6. Сетчатые модели и Х-файлы......Page 138
Часть II. 3D-объекты, движение и столкновения......Page 159
Глава 7. Динамика материальных точек......Page 160
Глава 8. Столкновения материальных точек......Page 187
Глава 9. Динамика твердых тел......Page 217
Глава 10. Столкновения твердых тел......Page 249
Глава 11. Сила тяжести и метательные снаряды......Page 282
Глава 12. Системы масс и пружин......Page 310
Глава 13. Вода и волны......Page 345
Часть III. Практические примеры......Page 375
Глава 14. Готовимся создавать игры......Page 376
Глава 15. Автомобили, корабли и лодки......Page 415
Глава 16. Авиация и космические корабли......Page 440
Эпилог......Page 470
Часть IV. Приложения......Page 471
Приложение А. Глоссарий......Page 472
Приложение В. Краткий обзор языка С++......Page 475
Приложение С. Основы программирования для Windows......Page 489
Recommend Papers

Физика для разработчиков компьютерных игр
 5947743175

  • 0 0 0
  • Like this paper and download? You can publish your own PDF file online for free in a few minutes! Sign Up
File loading please wait...
Citation preview

PHYSICS MODELING FOR GAME

PROGRAMMERS David Conger

Тl-IC>IVISON

*



COURSE TECHNOLOGV Professional



Trade



Reference

ПРОГРАММИСТУ

д. Конгер

ФИЗИКА ТЧИКОВ О Б А АЗ Р Р Я Л Д КОМПЬЮТЕРНЫХ ИГР Перевод с английского А. С.

Молявко

Москва БИНОМ. Лаборатория знаний 2007

К64 Конгер Д. К64

Физика для разработчиков компьютерных игр / Д. Конгер; Пер. с анrл. А. С. Молявко . - М . : БИНОМ. Лаборатория знаний, 2007. - 520 с . : ил. ISBN 5-94774- 3 1 7- 5 (русск . ) ISBN 1 - 59200-093-2 (англ .) Рассматриваются вопросы физического моделирования окружаю­ щего мира при разработке компьютерных игр. Кроме собственно

физики в книге приводятся примеры практического применения

физических моделей в играх. Описание простой платформы физиче­ ского моделирования затем переходит в плоскость изложения прин­ ципов моделирования отдельных физических явлений, применимых к играм. Рассматриваются вопросы программирования приложений с использованием созданных инструментов. Представленные в книге модели написаны на С++ с применением DirectX и компилировались в VS.NET. К книге прилагается компакт-диск, содержащий все примеры и необходимый инструмен'!'арий. Для чтения книги достаточно зна­ ния физи1ш и математики в пределах школьного курса и первично­ го опыта программщюваuиа на С++. Для программистов kомпьютерных игр, студе11тов и старше­ классников, интересующихся программированием.

УДК 530.1+004.7 ББК 32.973.202

Учебное издание Конгер Дэвид

Физика д.JJ:я разработчиков компьютерных игр Ведущий редактор А. С. Мол.явко Художник Ф. Инфантэ Художественный редактор О. Лапко Компьютерная верстка Л. П. Черепанова, Л. В. Катуркина Подписало в печать

26.09.06.

Гарнитура Школьная. Усл. печ. л.

Формат

70

х

1007{6

42,25. Тuраж 1500

экз. Заказ

5381

Издательство «БИНОМ. Лаборатория знаний» Адрес для переписки: 125167, Москва, ттроезд Аэропорта, 3 Телефон: (495)157-5272. E-mail: [email protected] http://www.Lbz.ru При участии 000 «ПФ �(Сdшко»

Отпечатано в ОАО } и главе 1 0 « Столкновения твердых тел>} вы познакомитесь с основами физики твердых тел.

Вращение 3D-объекты могут двигаться вперед или назад, влево или вправо, вверх или вниз. Однако, двигаясь, они могут еще и вращаться. Моделирование вращения увеличивает количество сил, которые игра должна приклады­ вать к объекту. Вращение может стабилизировать или дестабилизиро­ вать движущиеся объекты. Например , когда игрок в футбол (я говорю об американском футболе) бросает мяч, он непроизвольно бросает его так, чтобы мяч вращался. Вра­ щение стабилизирует полет мяча, и его легче поймать. Если вы пишете игру, в которой моделируется игра в футбол, моделирование вращения будет важным моментом.

Трение В реальном мире большинство движущихся объектов, в конце концов, останавливается из-за трения. Моделирование трения часто бывает необ­ ходимым и в играх. Я играл в игры, где персонажу приходится двигаться по обледенелым или просто скользким поверхностям. Игрок должен добиться, чтобы персонаж двигался в нужном направлении по этим по­ верхностям, а чтобы сделать жизнь интереснее, по персонажу обычно стреляют со всех сторон. Мне часто приходилось сталкиваться со случаями, когда программи­ сты теряли массу времени, пытаясь смоделировать трение. Они пытались свести все к правилу: «Если все выглядит нормально, значит, все нормаль­ но>}. Они писали программу и проверяли, правильным ли выглядело дви­ жение персонажа по скользкой поверхности . Если нет, они изменяли программу и снова проверяли. Поскольку они не опирались на реальную физику, им приходилось множество раз переписывать программу и про­ верять ее в работе. Они бы сэкономили массу времени, если бы просто ис­ пользовали в программах формулы из физики.

Физика в и грах

15

Сопротивление воздуха и воды Во многих играх сопротивление воздуха игнорируется вообще, однако никто этого не замечает. В прошлом игры выглядели правдоподобными и без моделирования сопротивления воздуха. Однако, похоже, это скоро отойдет в прошлое . Игры становятся все реалистичнее, и важность моде­ лирования сопротивления воздуха растет. Игнорировать сопротивление воды разработчики игр не могут. В лю­ бой игре, где персонажи и объекты двигаются в воде, возникает необхо­ димость реалистично моделировать сопротивление воды. Моделировать сопротивление воды значит не только замедлять дви­ жение в воде. Ведь вода может двигаться сама по себе. Возникающие при этом течения увеличивают сопротивление, если персонажи или объекты двигаются против этих течений. Течения увлекают за собой все в них по­ падающее. В некоторых играх движения в воде моделируются достаточно эффек­ тивно . Пример - серия игр Legend of Zelda. Главный персонаж часто дви­ жется в воде. При этом способ передвижения персонажа зависит от того, какими средствами на данный момент он располагает. Если у него есть волшебная маска, превращающая его в водное существо, он двигается бы­ стро. В противном случае его движения будут довольно медленными. Тече­ ния увеличивают или уменьшают сопротивление, когда персонаж плывет. Если в вашей игре встречается движение в воде, то нужно смоделировать сопротивление воды хотя бы на том же уровне, что и в этой серии игр.

С ила тяжести Сила тяжести влияет на все. От нее нельзя избавиться, даже в космосе. Не важно - бросает ли ваш персонаж гранату или ведет космический корабль к Марсу, на результат его действий влияет сила тяжести. Игра должна моделировать силу тяжести во всех ситуациях, поэтому моде­ лированию силы тяжести я посвятил целую главу: это глава 1 1 , « Сила тяжести и метательные снаряды*. В ней рассказано достаточно , чтобы можно было смоделировать силу тяжести почти во всех ситуациях.

Зам ечание Практически единстве нная ситуа ция, для которой я не описываю модели­ рование силы тяжести - это сила тяжести внутри черной дыры. Физиче ­ ские законы, которые нужны для моделирования этого случая, сли шком сложны для этой кни ги . Если ваш персонаж в и гре долже н попадать в чер­ ные дыры, можете без уг рызе ний совести обманывать и грока и програм­ мировать такое поведе ние , какое сочтете нужным. Игроку никогда не доводилось бывать в черной дыре , и он, скорее всего, поверит в то, что вы ему покажете

16

Глава 1

Столкновения и взрывы Что это з а игра без взрывов? Даже в миролюбивых играх вроде The Sims есть взрывы. Я не знаю, почему это так, но большинству игроков нравится видеть, как предметы врезаются друг в друга и взрываются. Именно поэто­ му мы так часто видим сталкивающиеся автомобили в фильмах. Похоже, что Голливуд потребляет солидную часть продукции автопромышленности. Невозможно смоделировать все аспекты столкновений и взрывов. Фи­ зические соотношения, работающие здесь , слишком сложны. :К счастью, это не так уж важно. Если мы сможем смоделировать основные силы и взаимодействия объектов в столкновениях и взрывах, все будет выгля­ деть нормально. А если все выглядит нормально, значит , все нормально .

Гибкие вещи Хотя обычно мы этого не замечаем, вокруг нас множество гибких вещей. Если я говорю « гибкие вещи » , вы, вероятно, представляете себе шесты для прыжков и тому подобное. В физике « гибкими вещами» будут, на­ пример, волосы и одежда. Представляли ли вы себе, как сложно смодели­ ровать движение прически идущей девушки? Смоделировать движение платья ничуть не проще. Долгое время моделирование одежды, волос и других гибких вещей было слишком сложным, чтобы они могли присутствовать в играх. Более того, это моделирование было настолько сложным, что его избегали в компьютерной графике и анимации. 3D- моделирование тех времен было достаточно хорошим, чтобы моделировать все , кроме одежды и волос. В результате появилось множество ЗD-мультфильмов о насеко­ мых. В этих мультфильмах нет ни одежды, ни волос. Однако недавние достижения позволяют моделировать в играх гиб­ кие вещи , включая одежду и волосы. Их движение можно сделать до­ статочно реалистичным.

Вол н ы Работая с водой, приходится иметь дело н е только с сопротивлением и течениями: на воде должны быть волны. В старых играх волны имити­ ровались медленным перемещением персонажа или камеры вверх и вниз. Но в современных 3D-играх такое не проходит. Нужен более реа­ листичный подход. Например, предположим, что вы пишете игру о гонках на мотор­ ных лодках, вроде Hyd ro Th under (аркадная игра). Если волна ударит лодку в лоб, лодку может подбросить вверх или даже перевернуть. Если волна ударит лодку в борт, лодка вполне может перевернуться . Результат ударов зависит от размеров волн , угла столкновения лодки с ними , веса и формы лодки и так далее. Все эти факторы нужно правиль­ но смоделировать в игре .

Ф изика в и грах

17

Ч то я д ол ж ен з на ть из ма тема т ики , ч тоб ы п иса ть и г ры ? Физика требует математики. Если вы не математический гений, не вол­ нуйтесь, я расскажу вам обо всех математических понятиях, которые вам потребуются, чтобы разобраться в этой книге. Вы познакомитесь с: о

основами геометрии треугольников;

О

векторами;

О

матрицами;

о

производными.

Основы геометри и треугол ы-Jи ко в :Компьютерная 3D-графика основана на треугольниках . Если вы собира­ етесь моделировать 3D-сцены и объекты, вы должны знать основные свойства треугольников. Например, нужно уметь найти длину стороны треугольника, зная длину двух других сторон и величину угла между ними.

Векторы Физика занимается вопросами взаимодействия сил и объектов. Силы очень удобно представлять с помощью векторов. Векторы дают удобный способ анализа сочетаний сил и определения сил, действующих на объекты.

Матрицы Программисты, работающие с 3D-объектами, обычно преобразуют векто­ ры сил в матрицы . Матрицы предоставляют изящный способ упрощенно­ го представления проблем. Они делают многие задачи 3D-графики более простыми для понимания и выполнения. Например, предположим, что нужно смоделировать поведение ящи­ ка, которое должно зависеть от того, куда приложено усилие - к ребру или к середине грани . Если усилие приложено к верхнему ребру, он дол­ жен перевернуться . Если оно приложено к середине боковой грани , он должен скользить по полу. Чтобы правильно смоделировать поведение ящика, нужно начать с анализа сил с помощью векторов. Затем нужно преобразовать векторы в матрицы и воспользоваться правилами умножения матриц, чтобы опре­ делить величины сил, действующие на вершины ящика. В результате можно определить, как будет двигаться ящик.

18

Глава 1

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

П роизводные Производные - это часть математического анализа. Да, математический анализ - не самая простая область математики, и он сложен для понима­ ния. Однако сам процесс использования производных можно упростить. Если вы не изучали математический анализ, я думаю , вы удивитесь тому, насколько простыми могут быть производные.

Ч то я дол ж ен зна ть и з пр о г р а м м и р ования ? Краткий ответ на этот вопрос: « Не слишком много » . Если вы можете на­ писать программу на С++ для Windows, то знаете достаточно, чтобы освоить эту книгу. Если вы изучали программирование на С++ в школе или институте, то поймете все, что мы будем рассматривать в этой книге. Если вы изучали программирование самостоятельно и занимались на­ писанием программ на С++ для Windows около года или больше, все бу­ дет в порядке. Для программирования графики мы будем использовать библиотеки Microsoft DirectX. О DirectX написаны целые книги. Вам не обязательно иметь опыт работы с ним . В этай книге о DirectX будет рассказано доста­ точно, чтобы вы смогли выполнять физическое 3D-моделирование, кото­ рому посвящена данная книга. Если вы хотите глубже изучить DirectX, попробуйте почитать, например, книгу Wendy Jones «Beginning Di­ rectX 9» (издательство Premier Press). Если вы приверженец OpenGL или какой-то другой графической библиотеки, не пугайтесь. Хотя в примерах этой книги используется DirectX, собственно физическое моделирование выполняется в коде, ко­ торый можно использовать практически с любой графической библиоте­ кой . Можно использовать этот код с OpenGL или чем-то еще , не внося в него больших изменений.

И то г и Чтобы создать реалистичную 3D-игру, программисты должны моделиро­ вать физические силы, действующие в природе . Это требует знания физи­ ки, математики и программирования 3D-графики. Обо всем этом и рассказывается в этой книге. Прежде всего, мы изучим основы 3D-про­ граммирования с помощью DirectX. Этому посвящена следующая глава.

Гла ва 2

И м ит а ци я З D - г р а фи ки с п о м о щ ь ю DirectX В этой главе мы познакомимся с API DirectX, созданным Microsoft. Di­ rectX - основной инструмент, используемый создателями игр и графиче­ ских программ для работы с 3D-графикой. Если вы уже знакомы с DirectX, возможно, вы захотите сразу перейrи к главе 3 , « Математиче­ ские инструменты » , и начать знакомиться с математикой, которая нам понадобится. Если вы впервые столкнулись с DirectX, то эту главу при­ дется прочитать. В ней есть: о

обзор DirectX и его возможностей;

о

введение в компоненты DirectX;

о

пошаговое руководство по подготовке DirectX к работе;

о

обзор операций, которые должна выполнить программа, завер­ шая работу с DirectX.

Ч то т а кое DirectX? DirectX - это интерфейс программирования приложений (API - Applicati­ on Programming Interface) , позволяющий разработчикам игр и графиче­ ских приложений выполнять мультимедиа-задачи, не привязываясь к конкретным типам аппаратных устройств. Это избавляет вас и меня от за­ бот о том, какие видеокарты и звуковые карты установлены в компьюте­ рах пользователей. Кроме того, DirectX предоставляет высокоуровневые функции для выполнения множества задач, связанных с 3D-графикой . Это позволяет нам с легкостью сконцентрироваться на собственно играх, а не на задачах генерации графики и звука. Необходим ли нам DirectX? Если коротко, то да. Чтобы понять, поче­ му, подумайте, как работает большая часть программ . Подавляющее большинство приложений большую часть своего време­ ни взаимодействуют с Windows , используя обработчики событий. Напри­ мер, Windows сообщает приложению, что была нажата кнопка Close в его окне или пользователь щелкнул левой кнопкой мыши в какой-то точке окна. Приложение реагирует на эти сообщения. Например, оно может по­ просить Windows создать окно или нарисовать линию. Windows выпол­ няет полученные запросы .

Глава 2

20

"У такого подхода есть свои преимущества. Он использует API, кото­ рый называется GDI (Graphics Device Interface - интерфейс графических устройств), чтобы позволить программистам писать программы, не беспоко­ ясь о том, какие графические карты установлены в компьютерах пользова­ телей. Он также вынуждает приложения аккуратно обращаться с другими приложениями. И, наконец, он позволяет пользователю легко копировать данные из одних приложений в другие. Но для игр GDI работает слишком медленно. GDI создан для рабочих приложений (например, для рисования диаграмм) , которые не слишком быстро изменяются во времени. Его невоз­ можно использовать для отображения 3D-графики в реальном времени.

Альте р нати вы DirectX DirectX - не единственный существующий и гровой API . У разных частей Di­ rectX есть серьезные соперники . Напри мер, библиотека OpenGL (Open Graphics Library - открытая графическая библиотека) - хорошая альтерна­ ти ва DirectЗ D, OpenAL (Open Audio Library - открытая аудио библиотека) альтернати ва DirectSound , а Berkeley Sockets может выполнять большинст­ во функций DirectPlay. Преи мущество этих API перед DirectX - возмож­ ность применения их на разных платформах. OpenGL можно использовать на машине под управлением Windows , на машине Apple или машине под управлением Linux , а DirectX работает только на компьютерах под управле­ нием Windows . Преи мущество Directx - при надлежность к вселенской и м­ перии ; DirectX работает хорошо на большинстве маши н , поскольку на большинстве машин используется Windows , и в поддержку Directx вклады­ вается много денег и усилий. Хотя эта книга концентрируется на использовании Directx для ЗD-моделиро­ вания , физика и описывающий ее код остаются неизменными при ис­ пользовании любого API . Можете использовать то, что вам нравится.

Д ва предста вления DirectX Microsoft делит интерфейс DirectX на два основных набора API. Один на­ бор - низкоуровневый и напрямую обращается к аппаратным устройствам. Если нужных аппаратных устройств в системе нет, этот низкоуровневый API имитирует их присутствие. DirectX также содержит набор высоко­ уровневых API, к которым можно обращаться через программные объек­ ты, содержащиеся в библиотеках DirectX.

Н изкоуровневое п редста вление: HAL и HEL DirectX позволяет программистам работать с аппаратными устройствами практически напрямую , при этом сохраняя аппаратную независимость, обеспечиваемую мультимедиа-стандартами Windows. Не важно, какие ви­ деокарты и звуковые карты установлены в компьютерах игроков - Di­ rectX позволяет их использовать. Команды DirectX преобразуются

И мита ц ия З D - графики

с

помо щь ю D i rectX

21

непосредственно в команды, понятные аппаратным устройствам в компь­ ютере пользователя . Рисунок 2. 1 показывает , как это делается. :Как ви­ дите, есть два компонента, отделяющие высокоуровневый API DirectX от аппаратных устройств: НАL (Hardware Abstraction Layer - слой абстра­ гирования аппаратуры) и HEL (Hardware Emulation Layer - слой эмуля­ ции аппаратуры) . Приложение

W1n32

Компоненты DirectX

HEL... слой эмуляции аппаратуры

HAL: слой абстрагирован и я аппаратуры

Аппаратные усrройства. в идеокарты, звуковые карты, джойстики и так далее

Рис. 2.1. Архитектура DirectX

HAL преобразует инструкции DirectX в инструкции аппаратуры . Чтобы игры работали как можно быстрее, DirectX пытается выполнять все задачи с помощью аппаратных устройств. Для этого он использует HAL всегда, когда это возможно . А что, если окажется, что аппаратура не поддерживает какую-то воз­ можность, запрошенную DirectX ? DirectX притворится, что эта возмож­ ность поддерживается аппаратурой. Да, именно так . Он воспользуется вторым низкоуровневым API - HEL. HEL эмулирует возможности, отсут­ ствующие в аппаратных устройствах компьютера. Это позволяет играм ра­ ботать, если DirectX требует больше, чем могут предоставить устройства компьютера. Но у эмуляции есть своя цена. HEL работает очень медленно.

В ысокоуровневое п редставление: компоненты DirectX В высокоуровневом представлении DirectX делится на несколько ком­ понентов, большинство из которых мы будем использовать, изучая мо­ делирование физики: о

Direct3D. Этот компонент отвечает за работу с графикой - как

2D, так и 3D. :Когда-то за 2D-графику отвечал DirectDraw, но Microsoft встроила его в Direct3D и переименовала в DirectX Graphics. Однако почти все называют его Direct3D. Direct3D позволяет работать с 2D-графикой, используя все возможности аппаратных устройств , которыми обладают 3D-графические карты. В этой книге мы будем использовать и 2D-, и 3D-графику.

Гл ава 2

22 о

Directlnput. Этот компонент обеспечивает подцержку мышей, клавиатур, джойстиков, трекболов и практически любых других устройств ввода. Microsoft говорит производителям устройств ввода: «Если вы хотите, чтобы они работали в Windows, лучше напишите для них драйверы под Directlnput » . Интерфейс Di­ rectlnput настолько абстрактен, что фактически производители могут создать под него драйверы для чего угодно - от трекболов до костюмов виртуальной реальности .

о

DirectPlay. Этот компонент обеспечивает сетевые многопользо­ вательские игры. Когда вы используете DirectPlay, не важно, подключаетесь ли вы к сети через модем, Internet-кaнaл, LAN или что-то еще; обо всех связанных с аппаратурой вопросах по­ заботятся за вас. В этой книге DirectPlay не рассматривается, но, став гением физики, можете использовать его для модели­ рования физики в многопользовательских играх.

о

Direct Sound. Этот компонент отвечает за работу с цифровым звуком. Он позволяет обращаться непосредственно к звуковой карте, не зная, какого она типа, и автоматически использует предоставляемые этой картой возможности ускорения и специ­ альные возможности. Также он поддерживает 3D-звучание и звуковые эффекты.

о

DirectMusic. Как явствует из названия, этот компонент воспро­ изводит музыку, но он делает и намного больше. Источники му­ зыки могут по-разному размещаться в 3D-среде, и их звучание может динамически изменяться. DirectMusic может даже созда­ вать композиции во время работы на основе элементов, которые вы ему передадите.

о

Direct Show. Этот компонент отвечает за запись и воспроизведе­ ние мультимедиа-потоков, например, MPEG, АVI и МР3 .

СОМ -объекты DirectX теоретически основывается на объектах, созданных с помощью модели Microsoft СОМ (Component Object Mod el компонентная модель объектов). СОМ - это абстракция, изобретенная для упрощения боль­ ших программных проектов. "Удачной ли была эта абстракция, каждый может решать сам. Идея состоит в том, что каждый СОМ-объект пред­ ставляет собой черный ящик, соответствующий какой-то части про­ граммы или аппаратному устройству. Чтобы создать программу, вы связываете между собой набор объектов . Доступ к СОМ-объектам осуще­ ствляется через интерфейсы. Иптерфейс (interface) это набор функ­ ций, называемых методами (methods). Большая часть сказанного будет звучать знакомо для программистов, работавших на объектно-ориентированных языках, например, С++ или Java. Собственно говоря, объекты СОМ совместимы с объектами С++ на -

-

И мита ц ия ЗD- г р а фики

с

п омо щь ю D irectX

23

бинарном уровне; в программах на с++ объекты сом могут использо­ ваться как обычные объекты. СОМ-объекты компонуются динамически во время выполнения про­ грамм. Это значит, что, в идеале, СОМ-объекты можно заменять в програм­ мах на новые объекты без необходимости перекомпилировать программу. Это полезно, если мы хотим обновить распространенную и широко исполь­ зуемую программу или большую систему. СОМ-объекты обладают достаточными возможностями , чтобы эта операция была эффективной. о У каждого СОМ-объекта и интерфейса есть уникальный 128-бито­ вый идентификапионный номер, который называется глобально уникальным идентификатором (GUID - Globally Unique IDenti­ fier). Созданная Microsoft программа GUIDEN.EXE генерирует эти идентификаторы, которые будут уникальными; скорее все­ го, никакие два СОМ-объекта или интерфейса, созданные кем угодно, где угодно и когда угодно, не будут иметь одинаковых идентификаторов. О

Обновленные версии СОМ-объектов должны поддерживать ин­ терфейсы предыдущих версий. При этом программы, в кото­ рых используется этот СОМ-объект, будут продолжать работать без перекомпиляции, даже если внутреннее содержимое объек­ та полностью изменилось.

о

СОМ-объекты содержат счетчик, отслеживающий количество активных ссылок на эти объекты. Если это количество равно О, ресурсы, выделенные объекту, освобождаются, и объект уничтожается.

СОМ является основой ActiveX, OLE и, что важнее всего для нас, DirectX. СОМ, ActiveX и .NET

Сначала Directx не был АР!, основанным на СОМ . Microsoft добавила СОМ к DirectX, купив этот АР! у создавшей его компании Reality Labs . В резуль­ тате присутствие СОМ в Directx не слишком навязч иво. Да, СОМ нужно ис­ пользовать для выделения и освобожден ия основных компонентов DirectX. Но кроме этого , больше возиться с СОМ не нужно. Не обязательно быть знатоком СОМ и разбираться в его тонкостях, чтобы использовать в своих програм мах Directx. Вы наверняка слышали о . N ET инициативе компании Microsoft. Все, что делает Microsoft, перетаскивается под вывеску . N ET. Это относится и к DirectX. Microsoft уже предоставила доступ к DirectX для программ . N ET. Интерфейс .NЕТ использовать проще, чем интерфейс СОМ . Однако исполь­ зование интерфейса . N ЕТ связано с некоторыми накладными расходами. Снижение скорости работы программ при использовании . N ET составит 2-5 %. Кроме того, может увел ич иться размер программ . Вам решать, сто­ ят ли увел ичение размера и падение скорости работы программ облегче­ н ия их разработки . -

24

Глава 2

И спол ьз ова ние DirectX Есть три способа, позволяющие запустить DirectX и воспользоваться его функциональностью. Первый - лобовой способ. Нужно создать набор пе­ ременных для инициализации и передать информацию из них функциям инициализации. Когда DirectX будет готов к работе, к его функциональ­ ности можно обращаться через его API . Второй способ - позволить Visual Studio сделать часть работы за вас. Когда вы устанавливаете DirectX SDK (Software Development Kit - набор разработки программ), он автоматически добавляет мастер DirectX App­ Wizard к Visual Studio. AppWizard создает для вас пустые DirесtХ-прило­ жения. Все, что остается сделать вам - добавить в них функциональность игр или графических программ. Третий способ - самый простой. Позвольте мне сделать за вас часть работы. По разным причинам DirectX АррWizard обладает некоторыми ограничениями. Есть и некоторые недостатки в его использовании . Поэ­ тому я создал оболочку исходного кода, которая запустит и подготовит DirectX к использованию. Использовать AppWizard и эту оболочку удобно, но есть вещи, кото­ рые нельзя сделать с их помощью. Поэтому важно разбираться в API Di­ rectX . И мы кратко рассмотрим инициализацию части DirectX - а точнее, Direct3D - лобовым способом. Это позволит нам узнать, как Di­ rectX работает в действительности. После этого я познакомлю вас с мас­ тером АррWizard и созданной мной средой.

И н и циал изация Di rectX лобовым с п особом Компоненты DirectX - это СОМ-объекты. СОМ-объекты реализуются в виде библиотек DLL (Dynamic Link Library - библиотека динамической компоновки). Когда вы играете в игру, использующую DirectX, эти биб­ лиотеки загружаются, и игра запрашивает из них нужные ей интерфей­ сы. Методы из этих интерфейсов и выполняют все операции рисования, работы со звуком и обработки ввода. Написание собственных СОМ-объектов возможно и, вероятно, полез­ но, но большинству программистов, пишущих игры, достаточно уметь использовать эти объекты, связанные с DirectX. На самом деле мы практически не будем иметь дела с СОМ-объектами DirectX. Microsoft знала, что СОМ-объекты в DirectX должны присутствовать в минималь­ ном количестве, чтобы DirectX получил распространение, и спрятала большую часть взаимодействия с СОМ в пару функций, содержащихся в библиотеках импорта. Это удобно, поскольку всю функциональность, которая вам может понадобиться в DirectX, можно получить, не работая прямо с СОМ.

И митац ия 30- граф и ки

с

помо щ ь ю D irectX

25

Н есколь ко сл ов о стил е офо р мления про гра м м Код в этом разделе оформлен в стиле , используе мом Microsoft. В следующем разделе мы будем рассматривать код, сгенерированный масте ром AppWi­ zard . Этот мастер тоже гене рирует код, оформленный в стиле Microsoft. Мой собственный стиль оформления кода довольно сильно отличается от стиля Microsoft. Тому есть свои причины, однако я не собираюсь стоять на­ смерть, если мой стиль вас не устраивает. В оставшейся части книги я буду использовать стиль Microsoft для кода, выполняющего инициализацию DirectX и завершение его использования. Все остальное будет оформлено в мое м стиле . Это поможет вам различать код, важный для приложения, от общего для всех игр кода инициализации и за верше ния. Использование СОМ-объекта DirectX состоит из четырех шагов. 1.

Объявление переменной, в которой будет храниться указатель на интерфейс объекта. Сначала этой переменной присваивает­ ся значение NULL:

LPDIRECTЗD

2.

g_pDЗD

=

3.

g_pDЗ D

=

NULL ;

Вызов функции создания объекта. Эта функция возвращает указатель на интерфейс объекта, который можно хранить в со­ зданной в шаге 1 переменной. Если функции не удается со­ здать объект, она возвращает значение NULL: Direct3DCreate9 ( DЗD_SDK_VERSION ) ;

Теперь, когда у нас есть указатель на интерфейс, можно исполь­ зовать его для вызова методов . Например:

g_pDЗD->GetAdapterDisplayМode { DЗDADAPТER_DEFAULT , ¤tdisplay) ;

4.

Завершив работу с СОМ-интерфейсами, нужно освобождать их в порядке, обратном порядку их инициализации. Невыполнение этой операции приведет к утечкам ресурсов, замедлению рабо­ ты систем и появлению кровожадных игроков, целью сущест­ вования которых является ваша преждевременная кончина.

g_pDЗD->Release ( ) ; g_pDЗD

=

NULL ;

Зам ечание Код, показанный выше , используется для инициализа ции D1rectЗD. Код для ини циализа ции других компонентов D1rectX имеет ту же структуру. Теперь вы знаете о СОМ достаточно, чтобы инициализировать Di­ rect3D. Попробуем это сделать.

Глава 2

26 ИНИЦИАЛИЗАЦИЯ DIRECTЗ D

Direct3D работает и с 2D- , и с ЗD-графикой . Это делает Direct3D самым важным для нас компонентом DirectX - обо всем, что происходит в иг­ рах, мы узнаем через экран монитора. По мере усложнения физических моделей мы будем все более интенсивно использовать Direct3D. А сейчас мы просто проинициализируем его и настроим экран. :Каждый раз, когда вы создаете проект, использующий DirectX, нуж­ но добавить к нему библиотечные файлы DirectX , которые вам понадо­ бятся. Если вы работаете в Visual Studio 6 , откройте меню Project и щелкните в нем на пункте Settings . Откроется диалоговое окно Project Settings . Щелкните на вкладке Link . Найдите текстовое поле Object Lib­ rary Modules. В этом поле введите имена библиотечных файлов, которые вам понадобятся:. Для: большинства приложений, использующих Di­ rect3D, будет достаточно ввести следующее: dxguid . lib d3d9 . lib d3dx9 . lib winmm . lib

Замечание Если вы используете Visual Studio .NЕТ, щелкните правой кнопкой на имени п роекта. Из открывшегося контекстного меню выберите пункт Properties. В появившемся окне щелкните на папке Linker и выберите в этой папке пункт lnput. Введите приведенный выше список библиотек в строке Additio­ nal Dependencies. В программу нужно включить заголовочный файл DirectX 9: #include

11 DirectX Version 9

Теперь создадим переменную, в которой будет храниться указатель на интерфейс. Большинство разработчиков игр делают такие переменные глобальными: LPDIRECTЗD9

q_pDЗD

=

NULL ;

11 Указатель на объект DirectЗD

Подска з ка Как бы я ни ненавидел глобальные переменные, избежать их использова­ ния при программировани и игр сложно. Передача параметров функциям, от которых требуется максимальная скорость работы, может замедлить игру до невозможности, если эти параметры нужно помещать в стек и изв­ лекать из него. Увы - глобальные переменные работают быстрее. Это не значит, что у вас нет выбора и придется использовать глобальные переменные во всех случаях. Будьте осторожны, выбирая, какие перемен­ ные сделать глобальными и как к ним обращаться . Глобальные перемен­ ные могут стать источником ошибок, которые будет трудно устранить.

И м и тац и я 30 - графи ки

с

27

помо щ ь ю D irectX

Я помещу собственно инициализацию DirectX в новую функцию, названную D irec tЗD ini t ( ) . Эта функция будет вызываться из функ­ ции Gameinit ( ) . Объекты Direct3D можно создавать с помощью функции DirectЗDC­ reate 9 ( ) . Я назову возвращаемое значение этой функции его официаль­ ным именем - IDirect3D 9 . Здесь I означает интерфейс (interface). 11 Получаем указатель на IDirect3D9 if

(NULL -- ( g__pDЗD

=

Direct3DCreate9 ( DЗD SDK VERSION )

)

)

return E_FAIL ;

Функции Direct3DCreate9 ( ) всегда нужно передавать значение DЗD SDK_VERSION. Других корректных значений нет. DЗD_SDK_ VERSION _ обновляется при обновлении DirectX. Передача этого параметра сообщает программе, с какой версией DirectX ей нужно работать. Функция возвращает NULL, если ей не удается создать объект Di­ rect3D. Если она возвращает NULL, функция Game ini t ( ) возвращает значение Е FAIL. Используя в программах СОМ, вы будете довольно часто встречать зна­ чения s_ок и E FAIL. Все методы СОМ-объектов возвращают 32-разряд­ _ ные целые значения типа НRESULT, сообщающие о результатах работы этих методов. Обычно возвращаются коды s_ок и E_FAIL, но иногда метод может вернуть нечто вроде Е_INVALDARG, если ему передали неправиль­ ные аргументы, так что будьте внимательны. Согласно принятым стандар­ там, при успешном выполнении методы возвращают коды, начинающиеся с S, а при неудаче - коды, начинающиеся с Е. Если вы захотите узнать, успешно ли выполнился метод, воспользуйтесь следующими макросами: о О

SUCCEEDED. Возвращает TRUE для кодов успешного выполне­

ния и FALSE

-

для кодов неуспешного.

FAILED. Возвращает TRUE для кодов неуспешного выполнения

и FALSE

-

для кодов успешного .

Поскольку мы возвращаем функции Game ini t ( ) либо значение s_ОК, либо значение Е_FAIL, то успешность выполнения этой функции можно выяснить в функции WinМain ( ) с помощью макроса FAILED: 11 Инициализация игровой консоли if

( FAILED ( Gameinit ( ) return

)

(0) ;

Если Gameini t ( ) возвратит код ошибки, WinМain ( ) выразит возму­ щение.

Глава 2

28 РЕЖИМ Ы ДИ С ПЛЕЯ

Теперь, получив интерфейс объекта, можно воспользоваться его метода­ ми . Для начала узнаем, какой сейчас используется режим дисплея: 11 Структура для хранения информации о текущем режиме дисплея

DЗDDISPLAYМODE currentDisplay ;

11 Получаем информацию о текущем режиме дисплея if

( FAILED ( g_pDЗD - >

GetAdapterDisplayMode ( DЗDADAPTER_DEFAULT ,

¤tDisplay )

)

return E_FAIL ;

П одска зка К методу интерфейса можно обратиться, указав имя интерфейса, за кото­ рым следуют два двоеточия ( : : ), а затем имя нужного метода. Поэтому, если я пишу IDirect3D 9 : : GetAdapterDisplayMode ( ) , я обращаюсь к методу GetAdapterD isplayMode ( ) интерфейса IDirect3D9 . Метод IDirect3D 9 : : GetAdapterDisplayMode ( ) принимает два па­ раметра. Первый - используемый адаптер . Значение DЗDADAPTER_ DEFAULT соответствует основному адаптеру. Второй параметр - указатель на структуру, в которой хранится ин­ формация о режиме дисплея. Посмотрим на определение этой структуры: typedef s truct _DЗDD I SPLAYМODE UINT Width ; UINT Height ; UINT RefreshRate ; DЗDFORМAT Format ; DЗDD I SPLAYМODE ;

Смысл первых трех параметров вполне очевиден. Это ширина и высо­ та изображения на дисплее в пикселях, например, 1 280 х 1024, и частота обновления кадров, например, 85 Гц. Последний параметр - формат поверхности. Он показывает, как вос­ принимается информация о каждом пикселе. Есть много разных форма­ тов, из которых только несколько подходят для дисплеев и видеостраниц (мы вскоре разберемся, что такое видеостраницы). В таблице 2 . 1 перечис­ лены эти типы.

И м ита ц ия 3 0 - граф и ки

с

п омо щь ю Directx

29

Таблица 2 . 1 . Типы DЗDFORMAT Форм ат

Значение

DЗDFМТ_A2R10 Gl 0Bl0

32-битовый формат пикселя, в котором по 1 0 битов используется для каждого цвета, а 2 б ита - для альфа-канала

DЗDFМT A8R8G8B8

32-битовый формат пикселя ARGB, в котором

по 8 битов используется для каждого цвета и еще 8 - для альфа-канала

DЗDFМТ_X8R8G8B8

32-битовый формат пикселя, в котором по битов используется для каждого цвета

DЗDFМТ_AlRSGSBS

1 6-битовый формат пикселя, в котором по 5 битов используется для каждого цвета, а 1 бит - для альфа-канала

DЗDFМТ_XlRSGSBS

1 6-битовый формат пикселя, в котором по 5 битов используется для каждого цвета

DЗDFМТ_RSGбBS

1 6-битовый формат пикселя, в котором 5 битов используется для красного цвета, 6 для зеленого и 5 - для синего

8

Если функция IDirect3D 9 : : GetAdapterDisplayMode ( ) отработала как должно, то информация о текущем режиме дисплея хранится в структуре currentDisplay.

П АРАМ ЕТ Р Ы ДИСПЛЕЕВ Все, что отображается на дисплее, копируется непосредственно из облас­ ти памяти, называемой текущей видеостраницей (front buffer). Эта об­ ласть может располагаться в основной оперативной памяти компьютера, но чаще она находится в памяти видеокарты . Когда программа (или Windows) желает отобразить на экране что-то новое, она изменяет со­ держимое этой области (буфера) , и графическая карта пересылает это содержимое монитору. Размер буфера зависит от разрешения монитора и используемой глубины цвета. Разрешения, которые можно использовать, ограничиваются монито­ ром, видеокартой и Windows. Если вы хотите использовать какое-то кон­ кретное разрешение, его должны поддерживать и монитор, и видеокарта, и Windows. Если вы играли в компьютерные игры или просто возились с настройками дисплея в Control Panel, вы, вероятно, знакомы с самыми распространенными разрешениями, например, 640 х 480, 800 х 600, 1024 х 768, 1 280 х 1024 и 1 600 х 1 200. Кроме разрешения дисплея, важна еще глубина цвета. Глубина цве­ та - это количество памяти, отвечающее одному пикселю на дисплее. "Большая часть этой памяти предназначена для хранения цвета пикселя.

Глава 2

30

Например , если используется 16-битовая глубина, каждый пиксель мо­ жет принимать 2 16 или 6 5 536 цветов. Если глубина 24 бита, то разных цветов может быть 2 24 или более 16. 7 миллиона . Это больше, чем могут различить ваши глаза! Экран монитора покрыт миллионами светоизлучающих элементов, объединенных в тройки. Каждая тройка содержит один красный светоиз­ лучающий элемент, один зеленый и один синий. Сочетая разные интен­ сивности свечения этих элементов, можно получить любой цвет. Такая система называется RGB (Red-Green-Blue - красный-зеленый-синий). -

З а м е ч ание В большинстве телевизоров и мон иторов с ЭЛТ эти тройки состоят из лю­ минофоров, светящихся , если их бомбардировать электронами. Такое ре­ шение стало стандартом в 1 930-х годах, когда создавалось цветное телевидение, и используется до сих пор, хотя в последнее время появля­ ются новые технологи и . Эти технологии работают по-разному, но, в об­ щем , все они тоже используют RGВ-цвета. Часто биты, используемые для хранения цвета, делятся между этими тремя излучателями. Например, в 1 6-битовом формате R5G6B5 5 битов предназначены для хранения данных об интенсивности красного цвета, 6 битов - зеленого и последние 5 битов - синего. Если люди не могут различить более 1 6 . 7 миллионов цветов , зачем использовать еще большую глубину цвета? Дело в том, что дополнитель­ ные биты можно использовать для хранения другой информации. Чаще всего в ней хранятся данные для альфа-канала, позволяющего реализо­ вывать эффекты прозрачности.

ПОВЕ РХ НОСТИ И П Е Р ЕКЛЮЧЕНИЕ В ИД ЕОСТРАНИЦ В Direct3D области памяти, соответствующие по размеру экрану, называют­ ся поверхностями (surfaces). Выполняя рисование с помощью Direct3D, вы на самом деле рисуете на поверхности неактивной видеостраницы (back buffer), а монитор в это время отображает содержимое активной видео­ страницы (front buffer). Неактивная страница не отображается; это про­ сто область памяти, имеющая тот же размер и ту же организацию, что и активная видеостраница. Когда вашей программе нужно изменить изоб­ ражение , она записывает новые данные в неактивную видеостраницу. Когда процесс рисования заканчивается, мы просто меняем указатели местами , и поверхность, бывшая неактивной видеостраницей, становит­ ся активной, а бывшая активной - становится неактивной. Этот процесс называется переключением видеостр аниц (page flipping) - (cм. рис. 2 . 2). Использование переключения страниц решает несколько проблем. Во-первых, если рисовать непосредственно на активной видеостранице , изображение может быть разорвано . Разр ыв (tearing) появляется, если содержимое активной видеостраницы изменяется в тот момент , когда мо­ нитор обновляет изображение на экране. При этом монитор отобразит

И митаци я 3 0 - граф и ки

с

помо щь ю DirectX

31

часть обновленного содержимого видеостраницы и часть необновленного. Если использовать переключение страниц, разрывы видны не будут, по­ скольку изменения не появятся в активной странице , пока программа не закончит рисовать. Изначально на мониторе отображается первая поверхность , а на второй - иде т рисование

о

П ервая поверхность Активная видеостраница

Вторая п оверхность Неактивная видеостраница

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

Активная видеостраница Неактивная видеострани ца

Рис. 2 . 2 .

Схема переключения видеостраниц

Кроме того, переключение видеостраниц позволяет перезаписывать содержимое видеостраницы, не отображая ее . Почему это может понадо­ биться? Например, это нужно в псевдо-трехмерных изометрических иг­ рах, скажем, в DiaЫo. Изометрические игры выглядят трехмерными, но в них позиция наблюдения фиксирована, и линия взгляда обычно нахо­ дится под углом о:коло 4 5° :к горизонту. Когда прорисовывается персонаж игрока и существа, стремящиеся его уничтожить, нужно проследить, чтобы более близкие объекты прорисовывались поверх более дальних .

32

Глава 2

Это не слишком сложно. Нужно просто прорисовывать первыми самые дальние объекты, а затем прорисовывать более близкие. Это хорошо по­ лучается, если рисовать на неактивной видеостранице и переключать ви­ деостраницы, закончив рисовать, но если рисовать на активной видеостранице, то иногда объекты заднего плана могут оказываться на­ рисованными поверх объектов переднего плана.

СОЗДАНИЕ УСТРО Й СТВА Теперь мы готовы использовать интерфейс IDirect3D9, чтобы создать еще один объект устройство ( device). Объект Direct3D был довольно абстрактен , но устройство - это гораздо более конкретная вещь, соответ­ ствующая аппаратному устройству: видеокарте. Все рисование, которое вы хотите выполнять с помощью этой графической карты, будет выпол­ няться через создаваемый сейчас интерфейс. Если вы захотите исполь­ зовать вторую видеокарту (это делают немногие игры), вам понадобится второе устройство. Чтобы создать устройство, нужно следовать той же процедуре, что и при создании других объектов. Сначала нужно объявить указатель на ин­ терфейс IDirect3DDevice 9 . Часто этот указатель делают глобальным, чтобы к нему можно было обращаться из любой точки программы, в част­ ности, из функции GameLoop ( ) , в которой будет выполняться рендеринг: -

LPDIRECT3DDEVICE9

g_pDevice

=

NULL ;

// Наше устройство рендеринrа

Устройство создается с помощью метода IDirect3D9 : : Crea teDe ­ vice ( ) . Этот метод использует довольно много параметров, как видно из прототипа: НRESULT CreateDevice ( UINT Adapter , DЗDDEVТYPE DeviceType , HWND hFocusWindow , DWORD BehaviorFlags , DЗDPRESENT PARAМETERS *pPresentationParameters ,

)

IDirect3DDevice 9 * * ppReturnedDeviceinterface ;

В первом параметре этой функции - Adapter указывается адаптер , которому будет соответствовать создаваемое устройство. В большинстве случаев этому параметру предается значение DЗDADAPTER_DEFAULT. В параметре DeviceType указывается, будет ли использоваться для рас­ теризации и расчета освещения аппаратное устройство или программа. Три возможных варианта перечислены в таблице 2.2. Растеризация (rasterizati­ on) это процесс преобразования геометричесюих фигур, например, линий и поверхностей, в пиксели, которые можно отобразить на экране. -

-

И митация 3 0 - гр аф и ки

с

п омо щ ь ю Di rectX

33

Таблица 2 . 2 . Возможные значения параметра DeviceType Значение

Смысл

DЗDDEVТYPE НАL

Использовать аппаратную растеризацию. Все эффект ы теней и освещения рассчитываются аппаратно, если это возможно. Это значение используется чаще всего

DЗDDEVТYPE НЕL

Все рассчитывается программно. Это значение просто является приказом использовать HEL. Это медленно, но иногда полезно при отладке

D ЗDDEVТYPE SW

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

Параметр hFocusWindow указывает, в каком окне будет выполняться рисование. Следующий параметр - BehaviorFlags - содержит несколько флагов общего характера, описывающих требуемое от устройства поведение. Один из этих флагов указывает, должны ли вершины или вертексы (ver­ tices) обрабатываться программно или аппаратно. В таблице 2 . 3 перечис­ лены наиболее распространенные значения этого параметра. Таблица 2.3. Флаги поведения для устройств Флаги

Значен и е

DЗDCREATE НARDWARE VERTEXPROCESSING

Вершины обрабатываются аппаратно

DЗDCREATE SOFТWARE VERTEXPROCESSING

Вершины обрабатываются программно

D ЗDCREATE MIXED_ VERTEXPROCESS ING

Смешанная обработка вершин; иногда DirectX будет использовать программы, а иногда аппаратные устройства

DЗDCREATE DISAВLE_ DRIVER МАNАGЕМЕNТ

Вместо драйвера распоряжаться ресурсами будет DirectЗD

DЗDCREATE МANAGED

Ресурсы перемещаются между оперативной памятью и ускорителем по мере надобности . Это освобождает приложение от возни с распределени ем памяти

DЗDCREATE МULTITНREADED

Это заставляет устройство обеспеч ивать безопасность при многопоточном использовании . При этом снижается производительность

34

Глава 2

Параметр pPresentationParameters функции Crea teDeYi ce ( ) это указатель на структуру (весьма сложную), которая указывает спо­ соб отображения результатов работы. Например, эта структура опреде­ ляет формат видеостраниц и то, рисует ли программа в ок:ве или в nолноэкранном режиме. Последний параметр функции CreateDevice ( ) - ppReturnedDevi ­ ceinterface. О н определяет адрес указателя н а интерфейс, который в ы создаете с помощью этой функции. Прежде чем вы сможете создать устройство, нужно заполнить струк­ туру pPresentationParameters. Вот ее объявление: 11 Струхтура дпя хранения информации о методе рендеринга

DЗDPRESENT_PARAМETERS

dЗdpp ;

С элементами структуры вы можете познакомиться в таблице 2 . 4 , но большую их часть мы установим в О с помощью макроса ZeroМemory ( } : 11 Инициализация dЗdpp

Zeroмemory ( &dЗdpp ,



О.

sizeof ( DЗDPRESENT_PARAМETERS )

) ;

Таблица 2 . 4. Элементы структуры DЗDPRESENT_PARAMEТERS Элемент

Описание

BackВufferWi dth , BackВufferHeiqht

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

BackВufferFormat

Формат неактивной видеостраницы. Этот элемент - того же типа DЗDFORМAT, который вы уже встречали в функции IDirect3D 9 : : GetAdapterDisplayMode ( ) . Он описан в таблице 2 . 1 . В полноэкранном режиме BackВuf:ferFormat устанавливает режим экрана. В оконном режиме его нужно установить в значение, соответствующее текущему режиму экрана. Microsoft утверждает, что это не обязательно, но зачем вам усложнять себе жизнь?

SackВu:f:ferCount

Количество неактивных видеостраниц. Корректные значения - О, 1 , 2 и 3. Обычно вам будет нужна только одна неактивная видеостраница, но можно создать и больше, если вам это понадобится. Если указать О неактивных видеостраниц, DirectX все равно создаст одну

И м и тация З D - гр афи к и

с

п омо щ ь ю Di rectX

35

Элемент

Описани е

MultiSampleType , MultiSampleQuality

Мультисэмплинг - это методика для выполнения сглаживания, и митации размытости быстро движущихся объектов и других эффектов

SwapEffect

Эти флаги о писывают, как будет выполняться переключение видеостраниц. В большинстве игр используется значение DЗDSWAPEFFECT_DISCARD. Оно сообщает DirectЗD, что сохранять содержимое неактивной видеостраницы после ее переключения в активную не нужно. Позволив D1rectЗD быть несколько небрежным с неактивными видеостраницами, мы можем выиграть в п роизводительности

hDeviceWindow

Дескриптор окна, в котором устройство будет выполнять рендеринг. Если он равен NULL, рендеринг будет в ы п ол няться в окне фокуса, указанном в функции IDirect3D9 : : CreateDevice ( )

Windowed

Если этот параметр установлен в TRUE, приложение работает в окне, если в FALSE приложение работает в полноэкранном режиме -

EnaЬleAutoDepth­ Stencil

Установка этого параметра в TRUE позволяет DirectЗD распоряжаться буферами глубины за вас

AutoDepthStencil­ Format

Тип буфера глубины, который вы хотите использовать, если вы установили в TRUE параметр EnaЬleAutoDepthStencil

Flags

Флаги, которые не поместились в другие параметры. Они нужны нечасто

FullScreen RefreshRateinHz

Частота обновления экрана в герцах. Эта частота может быть разной, но обычно мониторы обновляют изображение с частотой 75 Гц или выше. П рисвоив этому параметру значение о или DЗDPRESENT RATE DEFAULT, вы выберете заданную по умол'ЧаниюЧ астоту обновления. В оконном режиме нужно использовать частоту по умолчанию, но в полноэкранном режиме вы м ожете выбрать любое корректное значение частоты

Presentation Interval

Определяет, насколько быстро DirectЗD п редоставляет неактивную видеостраницу. Обычно этому параметру присваивается значение D ЗDPRESENT INTERVAL DEFAULT. Это нужно делать в оконном режи ме -

-

Глава 2

36

Это практически все, что нужно сделать, чтобы Direct3D заработал. Вызов функции IDirectЗD9 : : CreateDevice ( ) в программе будет, веро­ ятно, выглядеть примерно так: 11 Создаем устройство if

( FAILED ( g__pDЗD-> CreateDevice ( DЗDADAPTER_DEFAULT , DЗDDEVТYPE_ВAL , G_hМainWindow , DЗDCREATE_НARDWARE_VERTEXPROCESSING , &dЗdpp ,

&g_pDevice )

)

)

return E_FAIL ;

Этот вызов создает устройство, соответствующее адаптеру по умол­ чанию, использующее аппаратуру для выполнения растеризации, основное окно для вывода, выполняющее аппаратную обработку вер­ шин и структуру dЗdpp, которую мы только что рассмотрели . Функция IDirect3D9 : : CreateDevice ( ) поместит указатель на устройство в па­ раметр g_pDevice.

Подска з ка Аппаратная обработка вершин намного быстрее п рограм м н о й , но некото­ рые (довольно старые) видеокарты ее не поддерживают. Если у вас из-за этого возникают пробле м ы , замените флаг DЗDCREATE_НARDWARE_ VERTEXPROCESSING на DЗDCREATE SOFТWARE VERTEXPROCESSING.

ОСВОБОЖДЕНИЕ Р ЕСУРСОВ Вот и все! Direct3D официально инициализирован! С этого момента мы готовы начать моделировать и рисовать. Важно не забывать, что перед завершением выполнения программы нужно освободить интерфейсы в последовательности, обратной последовательности их инициализации . Сначала мы получили интерфейс IDirect3D9 в указателе g_pD ЗD, а за­ тем - интерфейс IDirect3DDevi ce 9 в указателе g_pDevice . Освобож­ даются они в обратном порядке с помощью метода Release ( ) : int Shutdown (void)

{ 11 Освобождаем ухазатель на IDirectЗDDevice9 . if

g_pDevice g_pDevice->Release ( ) ;

}

g__pDevice

=

О;

11 Освобождаем ухазатель на IDirectЗD 9 .

И митация ЗD- граф и ки if

с

помо щ ью DirectX

37

g_pDЗD ) g_pDЗD->Release ( ) ; g_pDЗD

} }

=

О;

return S_OK ;

Обратите внимание, что здесь указателям присваивается значение О , а н е NULL. И тот, и другой вариант правилен.

И нициализа ц ия DirectX с помо щ ью мастера DirectX AppWizard Использование мастера DirectX АррWizard очень упрощает запуск и подго­ товку Direct3D к использованию. Точнее говоря, мастер не только инициали­ зирует Direct3D. Он инициализирует и все остальные компоненты DirectX. Последовательность работы с мастером DirectX AppWizard в разных версиях Visual Studio несколько различна. Чтобы помочь вам начать, я опишу работу с ним и в версии 6, и в версии 7 .

ИСПОЛЬЗОВАН И Е МАСТЕРА DI RECTX APPWIZARD В VISUAL STUDIO б Вот как использовать мастер DirectX AppWizard в Visual Studio 6 : 1.

2.

3. 4.

В меню File выберите пункт New. Visual Studio отобразит окно New. Если вкладка Proj ects не открыта, откройте ее с помо­ щью мыши. На странице New Projects показан список проектов, которые можно создать в Visual Studio. Выберите в этом списке пункт DirectX АррWizard. В текстовом поле Project Name введите имя нового проекта. Пока назовем его просто InitDX. Щелкните на кнопке ОК. AppWizard будет отображать последовательность диалоговых окон , в которых нужно задавать nараметры DirectX-пpoeктa. В первом диалоговом окне указывается общая информация о приложении. В верхней части диалогового окна нужно ука­ зать, приложение какого типа вы хотите создать. Все прило­ жения, которые мы будем создавать в этой книге, будут однодокументными. "Убедитесь, что выбрано однодокументное приложение, прежде чем продолжить.

Глава 2

38 5. 6.

7.

Ни для одной программы из этой :книги вам не понадобятся Di­ rectMusic, DirectSound и DirectPlay. Создавая прое:кт, сбросьте соответствующие флаж:ки. По:ка н е добавляйте в проект ничего . Убедитесь, что флажки добавления меню и доступа :к реестру сброшены. Щелкните на кноп:ке Next. В появившемся диалоговом о:кне AppWizard спросит вас, с :ка­ кого приложения вы бы хотели начать. Выберите пустое при­ ложение (Blank). Затем щел:кните на кноп:ке Finish .

Теперь Visual Studio сгенерирует набор файлов для вашего проекта. Позже мы разберемся, что делать с этими файлами.

ИСПОЛЬЗОВАНИ Е МАСТЕРА DI RECTX APPWIZAR D В VISUAL STUDIO 7 Интерфейс мастера DirectX АррWizard в Visual Studio 7 выглядит немно­ го по-другому, но работает в основном так же, ка:к и в Visual Studio 6 . 1

.

2.

3.

4.

5. 6. 7.

8.

В меню File выберите пункт New, а в открывшемся подменю пункт Project. Visual Studio отобразит окно New Projects. В ле­ вой части этого окна есть список языков, которые поддерживает Visual Studio. Для каждого языка по:казан список типов прое:к­ тов, которые можно создать. Щелкните на пап:ке Visual С++ Projects, а затем выберите значо:к DirectX9 Visual С++ Wizard. Введите имя проекта и щелкните на кнопке ОК. В левой части появившегося довольно необычного диалогового о:кна содержится списо:к вкладок. Справа от этого списка - про­ странство для отображения содержимого этих вкладок . Щелк­ ните на ярлыке вкладки Project Settings . В верхней части этой вкладки AppWizard спрашивает , какое приложение вы хотите создать . Все приложения, которые мы будем создавать в этой книге, будут однодокументными. Убе­ дитесь, что выбран пун:кт Single document window. Н и для одной программы и з этой :книги вам н е понадобятся Di­ rectMusic, DirectSound и DirectPlay. Создавая прое:кт, сбросьте соответствующие флажки. По:ка н е добавляйте в проект ничего . Убедитесь, что флажки Menu bar и Registry Access сброшены. Щел:кните на ярлыке вкладки Direct3D Options. На этой вклад­ ке АррWizard спрашивает вас, с ка:кого приложения вы бы хо­ тели начать. Выберите пустое приложение - пункт Blank . Щелкните на кноп:ке Finish, и работа с мастером закончена.

И митация ЗD- граф и ки

с

п омо щ ь ю Di rectX

39

Если попробовать запустить только что созданное приложение, вы увидите, что оно отображает окно с синим фоном. В окне также выводит­ ся текст, сообщающий о работе приложения. Хотя мастер DirectX АррWizard весьма полезен, у сгенерированного им кода есть несколько недостатков. Во-первых, этот код весьма сложен. Если вы не знакомы с DirectX, разобраться в нем и понять, что этот код делает, будет непросто. К несчастью, вам придется это сделать . В сгене­ рированном коде есть несколько мест, которые нужно будет изменять при использовании этого кода. Потребуется немало времени, чтобы по­ нять, где эти места и какие изменения нужно сделать. Вторая проблема, связанная с кодом, сгенерированным DirectX App­ Wizard, тесно перекликается с первой. Чтобы создать игру, нужно моди­ фицировать несколько мест в этом коде. Они рассеяны в сгенерированном коде. Было бы удобнее, если бы можно было легко отделить ваш код от кода, сгенерированного мастером . Еще одна проблема - избыточность кода. Мастер ориентирован на ге­ нерацию кода для примеров, поставляемых с DirectX SDK. Он просто не создан для использования в качестве платформы для игр. В результате мастер вставляет в код возможности, которые не нужны играм. Напри­ мер, он добавляет диалоговое окно, позволяющее настраивать DirectX. Дополнительный код можно удалить, но , поскольку он весьма сложен, часто бывает трудно понять, что можно выбросить, а что - нельзя. Кроме того, код, сгенерированный мастером, не очень быстро выпол­ няется. Вероятно, вручную вы напишете более быстро выполняющийся код. Если вы знаток DirectX, то оптимизировать сгенерированный масте­ ром код для вас не составит труда. Если вы не слишком хорошо разбирае­ тесь в DirectX, это может быть очень сложно. И, наконец , сгенерированный мастером код не предназначен для ис­ пользования как учебное пособие, хоть и снабжен изрядным количеством комментариев . В этой книге проще демонстрировать, о чем идет речь, ко­ роткими фрагментами кода, делающими именно то, что нужно .

И н ициал иза ция DirectX с помощью платформы физического модел и рова н и я Чтобы избежать возни с I_· у А как же вычитание? Это операция, обратная сложению . То есть,

(а - Ь) + Ь = а

Это уравнение связывает вычитание и сложение. Можно использо­ вать для изображения вычитания тот же чертеж , поскольку вычитание связано со сложением! Сравните это выражение со следующим:

а - (а - Ь) = Ь

Посмотрите на рисунок 3. 13. Вы увидите, что результатом вычитания одного вектора из другого будет вектор, начальная точка которого совпада­ ет с начальной точкой первого, а конечная - с начальной точкой второго.

Рис. З. 1 З.

Вычитание векторов

В компонентной форме записать вычитание не составляет труда. Если компоненты вектора а (ах , �), а компоненты вектора Ь - (Ьх , Ь ), то век­ у тор а - Ь будет состоять из компонентов (ах - Ьх , ау - Ьу) · Операция вычи­ тания векторов не обладает коммутативностью. Вектор Ь - а указывает в направлении, противоположном направлению вектора а - Ь. :Классы векторов несложно расширить так, чтобы они реализовывали операции сложения и вычитания векторов. Первый шаг - добавить в определения классов прототипы методов сложения и вычитания . Эти прототипы приведены в листинге 3 . 3 . -

Листи н г 3 . 3 . Прототипы для методов сложения и вычитания векторов 1 2 3

4 5

// прототипы методов для :класса vector_2d vector_2d operator + (vector_2d &rightOperand) ; vector_2d operator - (vector_2d &rightOperand) ;

// прототипы методов для :класса vector_Зd

6

vector_Зd operator + (vector_Зd &rightOperand) ;

7

vector Зd operator - (vector Зd &rightOperand) ;

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

66

Гл ава 3

Листинг 3.4. Методы для сложения и вычитания векторов

2

// методы для класса vector_2d inline vector_2d vector_2 d : : operator + (vector_2d &rightOperand)

3

{

1

4

return (vector_2d ( x+riqhtOperand . x , y+rightOperand . y) ) ;

5 6 7 8 9 10 11

inline vector_2d vector_2d : : operator - (vector_2d &rightOperand) return (vector_2 d (x- rightOperand . x , y-rightOperand . y) ) ;

1 2 / / методы для JCJiacca vector_3d 1 3 inline vector_3d vector_3d : : operator + (vector_3d &rightOperand) 14 { 15 return (vector_3d ( x+ri9htOperand . x , y+rightOperand . y , 16 z+rightOperand . z ) ) ; 17 18 1 9 inline vector_Зd vector_3d : : operator - (vector_3d &rightOperand) 20 21 22

return ( vector_3d ( x - rightOperand . x , y-rightOperand . y , z -rightOperand . z ) ) ;

23

Все эти методы работают практически одинаково. Они создают безы­ мянные временные переменные, вызывая :конструкторы своих :классов. Затем в списках параметров выполняется сложение или вычитание. Такой подход позволяет добиться максимальной эффективности, поскольку боль­ шая часть :компиляторов С++ устранит безымянную переменную, заменив ее простым возвратом значения, получаемого в списках параметров :конст­ рукторов , причем сам :конструктор вызываться не будет. Еще одна причина эффективности этих методов в том, что они встраи­ ваемые. Лично мне не нравятся определения :классов, забитые кодом ме­ тодов этих :классов. Я помещаю в определения только типы элементов данных и прототипы функций. Однако мне не хочется терять эффектив­ ность, свойственную встраиваемым функциям, поэтому в заголовки функ­ ций добавлено ключевое слово inl ine везде, где это возможно. Если вы работаете в Visual С++, то использовать inline не так уж не­ обходимо. Visual С++ автоматически сделает встраиваемыми все функ­ ции, какие сможет. У него есть собственный алгоритм определения того, какие функции встроить . Когда вы :компилируете версию программы для распространения, Visual С++ автоматически применит этот алгоритм . Если вы компилируете версию для отладки, то функц1:1и не делаются встраиваемыми, чтобы их выполнение можно было пошагово отслежи­ вать с помощью отладчика.

67

М атем атические инструменты

Версии классов vector_2d и vector Зd с операторами сложения и вычитания векторов можно посмотреть в файле PммathLibV2 . h. Этот файл находится на компакт-диске в папке Source\ChapterO З . Помимо простых операторов сложения и вычитания векторов нам пригодятся и операторы += и -=. _

Зам ечание В языке С++ запись u += v эквивалентна записи эквивалентна u = u v.

u = u

+

v,

а запись u -=

v

-

В листинге 3 . 5 приведен код операторов += и -= для классов векторов. В файле PммathLibVЗ . h в папке Source\ChapterOЗ на компакт-диске содержатся версии этих классов с операторами += и -=. Листинг 3 . 5 . Операторы += и -= 1

// Вставить в к.пасс vector_2d

2 3

inline vector_2d vector_2d : : operator += (vector_2d &rightOperand)

{ x+=rightOperand . x ;

4 5

y+=rightOperand . y ; return ( * this ) ;

6 7 8 9

inline vector_2d vector_2d : : operator -= (vector_2d &rightOperand)

10 11

x-=rightOperand . x ;

12

y-=rightOperand . y ;

13 14

return ( * this ) ;

15 1 6 // Вставить в к.пасс vector 3d 1 7 inl ine vector_3d vector_3 d : : operator += (vector_3d &rightOperand)

18 { 19 20

x+=rightOperand . x ; y+=rightOperand . y ;

21

z +=righ tOperand . z ;

22

return ( *this ) ;

23 24 25 inline vector_3d vector_3d : : operator -= (vector_3d &rightOperand) 26 27 28 29 30 31 }

x-=rightOperand . x ; y-=rightOperand . y ; z -=rightOperand . z ; return ( * this ) ;

Гл ава 3

68

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

Умножение и деление вектора на с каляр Умножение векторов можно выполнять разными способами. Первый спо­ соб - умножение вектора на скаляр (число). При таком умножении изме­ няется длина вектора, но не его направление (см. рис. 3 . 14). При этом число может называться коэффициентом масштабирования. Произведе­ ние скалярного числа (а) и вектора (v) записывается в виде av. Операция умножения вектора на скаляр коммутативна, поэтому av = va. В двумерных и трехмерных декартовых системах координат векторы записываются с помощью компонентов (х, у) или (х, у, z), соответствен­ но. Скалярное умножение векторов в компонентной форме сводится к ум­ ножению каждого компонента на скаляр. Если компоненты вектора v (vx, vy , vz ) , то компоненты вектора av - ( avx, avy , avz ) . Кроме того, можно разделить вектор на скаляр. Это то же самое, что умножить вектор на обратную величину скаляра, то есть деление вектора на 2 уменьшает его длину в 2 раза.

Рис . 3. 1 4.

Масштабирование вектора

В листинге 3 . 6 приведен код операторов умножения и деления для обоих классов векторов . Листинг 3 . 6 . Операторы умножения и деления для обоих классов

векторов 1 inline vector_2d vector_2 d : : operator * ( scalar rightOperand) 2 З return (vector_2d (x*rightOperand , y*rightOperand) ) ; 4 5 б inline vector 2d operator * ( s calar leftOperand , 7 vector_2d &rightOperand) 8 9 return (vector_2d ( left0perand*right0perand . x , 10 leftOperand*rightOperand . y) ) ; 11 12 1 3 inline vector_2d vector_2 d : : operator *= ( scalar rightOperand) 14 15 x*=rightOperand ; 16 y*=rightOperand ; 17 return ( * this ) ; 18

М атем атические инструменты 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64

69

inline vector_2d vector_2 d : : operator / ( s calar rightOperand)

{ return (vector_2d (x/rightOperгnd , y/rightOperand) ) ;

inline vector_2d vector_2 d : : operator /= ( scalar rightOperand) { x/=rightOpera.nd ; y/=rightOpera.nd; return ( *thi s ) ;

} inline vector_3d vector_3d : : operator * ( s calar rightOperand)

{ return (vector_3d (x*rightOperand , y*rightOperand, z*rightOperand) ) ;

} inline vector_3d operator * ( scalar l e ftOperand , vector_3d &rightOperand) { return (vector_3d (left0perand*right0perand . x , leftOperand*rightOpera.nd . y , leftOperand*rightOpera.nd . z ) ) ;

inline vector_3d vector_3d : : operator *= ( scalar rightOperand)

{

x*=rightOperand ; y*=rightOperand ; z *=rightOperand ; return ( * thi s ) ;

} inline vector_3d vector_3d : : operator / ( s calar rightOperand) { return (vector_3d (x/right0perand, y/right0perand , z/right0perand) ) ; } inline vector_3d vector_3d : : operator /= ( scalar rightOperand)

{

x/=rightOperand ; y/=rightOperand ; z /=rightOperand ; return ( *this ) ;

Поскольку скалярное умножение коммутативно, в каждом классе есть по две версии оператора умножен:и:я . Первая версия использует век­ тор в качестве левого операнда и скаляр - в качестве правого . Вторая вер­ сия использует в качестве левого опера11да скаляр, а в качестве правого -

Глава 3

70

вектор. В бинарных операциях языка С++ функция-оператор всегда вы­ зывается левым операндом. Соответственно, если в выражении вектор бу­ дет левым операндом, умножение пройдет без проблем. Однако если левый операнд - скаляр, то он не сможет вызвать функции-операторы классов векторов. Поэтому операции умножения, использующие скаляр в качестве левого операнда, нельзя реализовать как методы классов. Они должны быть дружественными функциями для этого класса. Прототипы двух этих функций объявлены в классах vector_2d и vector Зd так, как показано ниже: 11 Из :класса vector_2d friend vector 2d operator * ( scalar leftOperand , vector_2d &rightOperand) ;

11 Из :класса vector Зd friend vector_Зd operator * ( scalar leftOperand , vector_Зd &rightOperand) ;

В строках 6 - 1 1 листинга 3 . 6 приведен код дружественной функции­ оператора умножения для класса vector_2d. Заметьте, что ключевое слово friend не нужно ставить в первую строку функции. Оно должно присутствовать только в прототипе функции в определении класса. В листинге 3 . 6 также приведен код операторов *= для каждого класса векторов. Кроме того, в листингах приведен код операторов / и /=.

С калярное произведение векторов Кроме умножения вектора на скаляр, вектор можно умножить и на дру­ гой вектор. На самом деле умножать вектор на вектор можно нескольки­ ми способами. Один из них называется скалярным произведением (scalar product}. Это произведение двух векторов не следует путать с результа­ том умножения вектора на скаляр.

Замеч а н ие Результат умножения не может зависеть от того, какую вы выбрали систе­ му координат. Это основное ограничение для умножения. Скалярное про­ изведение следует этому правилу; независимо от используемой системы координат скалярное произведение одних и тех же векторов будет одним и тем же числом. Иногда скалярное произведение называют внутренним произведением (inner product) . Вычисление скалярного произведения векторов - это не то же самое, что вычисление результата умножения вектора на скаляр. Вы­ числяя скалярное произведение, мы перемножаем два вектора, чтобы в ре­ зультате получить скаляр. Скалярное произведение записывается так:

71

М атем атические инст рументы В компонентной форме скалярное произведение выглядит так:

для двумерных векторов а • Ь = ахЬх + ауЬу +

для трех мерных векторов

а •

для четырех мерных векторов

azbz Ь = ахЬх + �Ьу + a b + �bw z z

Если компоненты векторов неизвестны, то скалярное произведение этих векторов можно вычислить. Если известны магнитуды этих векто­ ров и угол между ними (О), как показано на рисунке 3 . 1 5 , то скалярное произведение можно найти по формуле

а • Ь = аЬ cos(B)

Рис. 3 . 1 5 .

Скалярное произведение векторов

Заметьте, что в этой формуле буквы а и Ь (не выделенные жирным шрифтом) - это магнитуды векторов а и Ь соответственно. Скалярное произведение векторов коммутативно, поэтому а • Ь = Ь • а. В этом можно убедиться, используя любое из приведенных выше уравне­ ний. Скалярное произведение векторов также обладает дистрибутивно­ стью это значит, что а • (Ь + с) = а • Ь + а • с. В листинге 3 . 7 приведен код методов классов vector_2d и vec­ tor_Зd для вычисления скалярного произведения векторов. -

Листинг 3 . 7 . Методы , вычисляющие скаля рное произведение векторов

2

11 Из 1СЛасса vector_2d inline scalar vector_2d : : Dot ( const vector 2d &vl )

з

{

1

return (x*vl . x + y*vl . y) ;

4 5 6 7 8 9

10 11

/ / Из 1СЛасса vector Зd inline scalar vector_Зd : : Dot ( const vector Зd &vl )

{ return (x*vl . x + y*vl . y + z *vl . z ) ;

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

а • а

=

аа cos(O) = а

2

Глава 3

72

a= i a i = �

а=�ахах +ауа у

Разумеется, если скалярно умножать вектор сам на себя, то угол бу­ дет равен О. Поскольку косинус нуля равен 1 , этот член можно игнориро­ вать. В результате мы получаем уршзнение, похожее на то, которое получали, обсуждая треугольники. Это теорема Пифагора. В математике множество таких совпадений. Выражение для вычисления нормы б:ыло дано для двумерных векторов, но оно работает и для трехмерных векторов. В этом случае оно примет вид

Следующий шаг - воплотить эти формулы в коде программ . Однако прежде чем мы этим займемся, сделаем важное замечание: если вы може­ те обойтись квадратом нормы, сделайте это. Квадратные корни вычисля­ ются очень медленно. Именно поэтому в библиотеке присутствуют два метода для вычисления норм: один для вычисления собственно нормы, а второй - для вычисления квадрата нор:мы. Код этих методов приведен в листинге 3 . 8 . Листинг 3 . 8 . Вычисление н о р м векторов 1 2 3 4 5

/ / Из :класса vector_2d inline scalar vector_2 d : : Norm (void)

{

return ( sqrt (x*x + у*у) ) ;

б

7

inline scalar vector_2d : : NormSqua:red (void)

8

9 10 11 12 13 14 15 16 17 18 19 20 21

return (x*x + у*у) ;

/ / Из :класса vector Зd inline scalar vector_Зd : : Norm (void) { return ( sqrt (x*x + у * у + z * z ) ) ;

inline scalar vector_З d : : NormSqua:red (void) return (x*x + у*у + z * z ) ;

73

М атем атические инструменты

Прежде чем завершить обсуждение скалярных произведений, замечу, что с их помощью можно определить, перпендикулярны ли друг к другу (или ортогональны) два вектора, то есть равен ли 90° угол между ними. Скалярное произведение ортогональных векторов равно О. Поэтому, если функции из листинга 3 . 8 возвращают О, вы будете знать, что векторы, использованные в качестве аргументов, ортогональны друг к другу.

Векторное п роизведение векторов При скалярном произведении двух векторов результат является скаля­ ром. При векторном произведении двух векторов результатом будет век­ тор. Векторное произведение записывается в виде:

а х Ь В отличие от скалярного произведения и других операций с вектора­ ми, рассматриваемых в этой главе, векторное произведение существует только в трехмерных координатах. Для любых двух векторов можно найти плоскость, в которой они ле­ жат, как показано на рисунке 3 . 1 6 . Вектор, получаемый при векторном произведении двух векторов, будет перпендикулярен плоскости, в кото­ рой лежат эти два вектора. Получаемый вектор называется нормальным вектором (normal vector) . Именно для его получения и предназначено векторное произведение векторов.

У4'!"' v

Рис. 3. 1 6 .

Два вектора и плоскость, в которой они лежат

Предположим, что два вектора определяют горизонтальную плос­ кость. В какую сторону - вверх или вниз - будет направлен нормальный вектор? Чтобы ответить на этот вопрос, воспользуемся правилом правой рук:и. Это правило сводится к следующему: чтобы определить направле­ ние, в котором указывает нормальный вектор, вытяните правую руку в направлении первого исходного вектора и направьте пальцы в ту сторону, в которой относительно первого исходного вектора расположен второй, как показано на рисунке 3 . 1 7. Отогните большой палец. Он будет указы­ вать в направлении, в котором должен указывать нормальный вектор. Для вычисления векторного произведения векторов, которые мы обо­ значим и и v, служит следующая формула:

U Х v = (UyVz - UzVy)i + (uzvx - uxvz)j

+

(uxvy - UyVx )k

В этой формуле i, j и k это единичные векторы, с которыми вы скоро познакомитесь. А пока мы приведем формулу к виду, который немного легче использовать для написания кода на С++: -

74

Глава 3

Рис . З. 1 7. Правило правой руки для векторного произведения векторов

r = u x v

rz

=

uxvy - uyvx

Эти уравнения дают нам компоненты вектора r, получающегося в ре­ зультате векторного перемножения векторов u и v . По этим формулам работает метод вычисления векторного произведения , код которого при­ веден в листинге 3. 9 . Листинг 3 . 9 . Метод выч исления векторного прои з ведения векторов 1 2 з 4

5 6 7 8 9

inline vector Зd vector_Зd : : Cross ( cons t vector Зd &rightOperand) return ( vector_Зd ( y*rightOperand . z - z*rightOperand . y , z* rightOperand . x - x*rightOperand . z , x*rightOperand . y - y*rightOperand . x) ) ;

Един ичные векторы Есть еще один способ компонентного представления векторов, который иногда бывает очень полезным. Можно определить двумерную декартову систему координат с помощью двух маленьких векторов длиной 1 , один из которых направлен вдоль оси х, а другой - вдоль оси у, как на рисунке 3 . 1 8 . Обозначим эти векторы х и у (читается как «Х с шапочкой» и «у с

75

М атем атические и нст рументы

шапочкой}> ) . Шапочка (символ ) означает, что это единичные векторы (unit vectors), нормы которых равны 1 . В их координатной системе ком­ поненты вектора к есть ( 1 , О), а вектора у - (0, 1). л

л

у л

х

Рис. 3 . 1 8 . Единичные векторы в двумерных координатах

Иногда единичные векторы обозначаются не х и у, а f и f. В любом случае это векторы с нормой 1 , направленные вдоль осей х и у. Любой вектор в декартовой системе координат можно представить в виде суммы произведений единичных векторов и скаляров. Другими сло­ вами, для любого вектора

v = ах + Ьу для некоторых а и Ь. Здесь а и Ь есть координаты вектора в системе коорди­ нат, определенной единичными векторами х и у. Поэтому если а = 2 и Ь 3, то координаты вектора равны (2, 3), и вектор можно записать в виде 2х + Зу или 2i + Зj. Единичные вектор ы для третьего и четвертого измерений обыч­ но обозначаются z и w или k и m . Их компоненты равны соответственно (О, О, 1) и (О, О, О, 1). =

Зам ечание Обозначение ( w или m ) нечасто встречается в книгах по математике. Чаще оно применяется в программировании ЗD-графики. Иногда требуется найти единичный вектор, направление которого совпадает с направлением другого вектора. Операция нахождения такого вектора называется нормализацией. Она довольно проста. Если разде­ лить вектор на его длину, вы получите единичный вектор с тем же на­ правлением, что и исходный. Единичный вектор обозначается так же, как и исходный, но с шапочкой :

v v= -v = --v .Jv • v л

Глава 3

76

В листинге 3 . 1 0 приведен код методов нормализации векторов для классов vector 2d и vector Зd. Листинг З. 1 О. Методы нормализации векторов 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

inline vector_2d vector_2 d : : Normalize ( scalar tolerance) { vector_2d result ; scalar length = Norm ( ) ; if ( length>=tolerance) {

resul t . x = x/length ; resul t . y = y/ length; return (result) ; }

inline vector_3d vector_3d : : Normalize ( scalar tolerance) vector_3d result ; scalar length = Norm ( ) ; if ( length>=tolerance) {

result . x x/length ; resul t . y = y/length ; z / length ; resul t . z return ( result) ;

Вероятно, вы заметили, что эти функции проверяют длину вектора. Если она меньше минимальной, указанной в переменной tolerance, то функции не выполняют деление, а просто возвращают значение О (деле­ ние на О, скорее всего, приведет к аварийному завершению выполнения программы).

П роецирование Скалярное умножение вектора н а единичный вектор называется проеци­ рованием или определением проекции вектора на единичный вектор. Проецирование выделяет компонент вектора, направленный параллель­ но единичному вектору, как показано на рисунке 3 . 19. Например, перемножая вектор v с единичным вектором х мы получим компонент х вектора v, то есть, vx, как показано ниже: л

,

v •x=(v x +v уул+v z)•x=v x•x+vу ул •x+v z•x=v х

z

х

z

х

77

М атем атические инструменты \

\ \\

у

'\ Рис . 3 . 1 9 .

вектор n

Проецирование вектора

v

на единичный

П РИМЕР: ОТСКАКИВАН И Е ОТ СТЕН Ы Проецирование векторов часто используется для моделирования столк­ новений объектов. Например, можно смоделировать столкновение мяча со стеной. Эта задача называется отражением вектора (vector reflecti­ on). Одна из первых компьютерных игр Ропg была имитацией настольно­ го тенниса, основанной на отражении двумерных векторов. Чтобы можно было выполнить моделирование, нам понадобятся два вектора, один из которых описывает движение мяча, а второй - располо­ жение стены. Определим единичный вектор, перпендикулярный стене, как на рисунке 3 . 20. Обозначим его n . Вектор v будет описывать переме­ щение мяча (его скорость и направление движения). Еще один вектор v' (читается как « и-штрих » ) будет описывать движение мяча после отскока от стены.

v

Рис . 3.20.

Отскакивание от стены

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

78

Глава 3 Зам е чание Когда говорят, что столкновение является упругим , то подразумевается, что стена твердая, а сталкивающийся с ней предмет представляет собой нечто вроде теннисного мяча, который отскочит от стены после столкно­ вения . Если со стеной столкнется комок грязи или снежок, то отскока не будет, и такое столкновение называется неупругим . Мы поговорим о стол кновениях подробнее, когда доберемся до посвященной им главы.

Поскольку в нашей задаче моделируется упругое столкновение, нам ' нужно только найти компонент вектора v , перпендикулярный стене, то есть компонент, направленный параллельно вектору n . Компонент век­ ' тора v , параллельный стене, можно просто скопировать из такого же компонента вектора v. Чтобы найти компонент вектора v в направлении вектора n , нужно найти скалярное произведение вектора v и вектора n и нормализовать его ПО формуле (v • n) п . Будет полезно присвоить обозначение компоненту вектора v, направленному параллельно стене, хотя нам никогда не пона­ добится его вычислять. Обозначим его р. Вся информация, которая у нас есть в данный момент, изображена на чертеже (см. рис. 3 . 2 1).

Поместим на чертеж всю наличную информацию

Рис. 3 . 2 1 .

Теперь вектор v можно записать в виде суммы компонентов:

v = р + (v • П

)n

Важно заметить, что в этой формуле р одинаков и для Компонент меняет знак на противоположный. Поэтому

v = p - (v • n •

р

л

v,

и для

' v .

)n л

А теперь найдем р из первого уравнения и избавимся от присутствия во втором уравнении:

79

М атем атические и нст рументы

v'= v - (v • n ) n - (v n ) n = v - 2(v . n )n •

Поскольку мы не использовали какую-то конкретную систему :коор­ динат, получе1шый нами результат верен для любой системы :координат. Здорово , правда?

ОТСКАКИВАНИ Е ОТ СТЕНЫ: П РАКТИЧЕСКАЯ ПРОВЕРКА Дабы убедиться в верности полученного результата, проверим: его. Для этого подставим в формулу реальные числа и проведем вычисления, что­ бы получить результат. Предположим, что игрок стреляет, и пули рикошетят от стены. Ком­ поненты вектора скорости пуль равны (-3 м/с, 4 м/с). Единичный век­ тор, перпендикулярный :к стене, обозначен i . Скорость пули есть магнитуда вектора ее скорости . Поэтому первый

наш шаг - нахождение нормы v вектора скорости пули

v.

V (- 3 м/с, 4 м/с) • (-3 м/с, 4 м /с) = V (-3 м/с)(-3 м/с) + (4 м /с)(4 м /с) = V 9 м2 /с 2 + 16 м2 /с 2 = V 25 м2/с2 =

= 5

м/с

Конечно, 5 метров в секунду - очень медленно для пули. Но для на­ шей задачи это не слишком существенно. Перед тем как найти вектор скорости пули после ее столкновения со стеной, найдем: проекцию вектора v в направлении вектора i . Затем можно подставить v и i в выведенные выше формулы: v •

i

v'

=

( (-3 м / с) i

=

-3 м/с

=

v

=

(-3 м/с) i

-

= (3

+

(4 м / с )

У ) i •

2(v i ) i •

+

(4 м/с) у - 2 (-3 м/с) i

м/с) i + (4 м/с) у

Компонент у (параллельный стене) не изменился, а знак :компонента х изменился на противоположный.

80

Глава 3

Вектор ы в DirectЗD Поскольку векторы очень важны в 3D-программировании, то вспомога­ тельна.я библиотека D3DX для Direct3D содержит несколько структур и функций дл.я работы с двумерными, трехмерными и четырехмерными векторами . Хот.я в библиотеке физического моделирования есть все, что нам понадобится для работы с векторами , учтите, что с ними можно рабо­ тать и с помощью Direct3D. Мы будем использовать функции библиотеки Direct3D, работая с 3D-графикой, но дл.я физических расчетов мы будем пользоваться только нашей библиотекой физического моделирования. Это позволит нам сделать большую часть кода легко переносимой между разными платформами. Если вы захотите применять дл.я написания игр вместо DirectX что-то другое, вы сможете использовать большую часть кода из этой книги. Структуры дл.я векторов называются DЗDXVECTOR2 , DЗDXVECTORЗ и DЗDXVECTOR4 для двумерных, трехмерных и четырехмерных векторов соответственно. В этих структурах под каждый компонент вектора отведена одна переменна.я типа float. Чтобы эти структуры можно было использовать в библиотеке моделирования, нам нужна возможность преобразования классов векторов из библиотеки моделирования в структуры векторов из DirectX и обратно. В библиотеке моделирования уже есть функции преоб­ разования из структур DЗDXVECTOR2 и DЗDXVECTORЗ в объекты классов vector 2d и vector Зd. Это конструкторы классов. Дл.я преобразования объектов классов vector_2d и vector Зd в структуры DЗDXVECTOR2 и DЗDXVECTORЗ нам понадобятся операторы приведения типов в классах. Код этих операторов приведен в листинге 3 . 1 1 . _

_

_

Листинг 3 . 1 1 . Операторы приведени� типов 1 2 З 4 5

vector_2 d : : operator D ЗDXVECTOR2 ( ) { return ( DЗDXVECTOR2 ( x , y) ) ;

б 7

vector_Зd : : operator DЗDXVECTORЗ ( ) { return ( DЗDXVECTORЗ (x , y , z ) ) ;

8 9

Чтобы выполнить преобразование, эти два оператора создают безы­ мянные временные переменнь1е типов D3DXVECTOR2 и DЗDXVECTORЗ , за­ писывают в них компоненты векторов и используют эти переменные в качестве возвращаемых значений .

81

М атем атические инструменты

М а тр и ц ы Матрицы - это просто массивы чисел, которые можно складывать и пере­ множать по определенным правилам. Вы, вероятно, знаете, что такое мас­ сивы в программировании, даже если вы не встречались с ними где-то еще. В физике и математике матрицы обозначаются заглавными буквами, на­ пример, М. Элементы матрицы обычно обозначаются строчными буквами с двумя нижними индексами, поэтому матрица может выглядеть так: 1

ill1 2

m 3 1

m14

ill2 1

ill22

m 2з

m 24

Ш3 1

ffi32

ffi33

ffi34

ffi4 1

ffi42

ffi43

ffi44

ill1

М=

В математике и физике нумерация столбцов и строк в матрицах обыч­ но начинается с единицы. Однако в компьютерах для хранения двумерных матриц обычно используются двумерные массивы, а индексы в массивах в языке С++ начинаются с О . Поэтому во многих книгах на компьютерные темы нумерация столбцов и строк в матрицах тоже начинается с О. Имен­ но так нумеруются столбцы и строки в матрицах и в этой книге. Матрица М называется матрицей 4 х 4 , поскольку в ней четыре столбца и четыре строки. В программировании графики на компьютере чаще всего используются квадратные матрицы, в которых количество строк равно ко­ личеству столбцов. Это утверждение верно и для тех областей физики, с которыми мы будем иметь дело. Почти все матрицы, которые нам встре­ тятся, будут квадратными. В библиотеке моделирования реализованы классы matrix2x2 и matrixЗxЗ для работы с матрицами 2 х 2 и 3 х 3 . С многих точек зрения матрицы проще векторов. Они не привязаны к ка­ ким-то координатам, как векторы. Они больше похожи на компоненты век­ торов в определенных системах координат. Собственно говоря, компоненты n-мерных векторов ведут себя очень похоже на матрицы размера 1 Xn. Учтя эту схожесть матриц и компонентов векторов , вероятно, вы не удивитесь, узнав, что над матрицами можно выполнять почти такие же операции, как и над векторами : сложение, умножение на скаляр и умно­ жение на другую матрицу. Однако будьте внимательными. У матриц есть свои особенности.

Замечание Не так уж сложно создать класс, который сможет работать с матрицами любого размера. Если вы хотите создать такой класс, прочитайте, напри­ мер, книгу «Numerical Recipes for С++» (издательство William Press and Сатрапу) . Однако мы сейчас изучаем специфическую область программи­ рования - программированию игр. Нам понадобятся только матрицы опре­ деленных размеров. Универсальность стоит принести в жертву скорости работы . Реализуя только те матри цы, которые нам понадобятся, можно упростить код и немного ускорить его выполнение.

82

Глава 3 В листинге 3 . 1 2 приведены определения классов матриц.

Листинг 3 . 1 2 . Классы matrix2x2 и matrix3x3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

class matrix2x2 private : scalar elements [ 2 ] [ 2 ] ; puЫic : matrix2x2 (void) ; matrix2x2 ( scalar initial izationArray [ 2 ] [ 2 ] ) ; matrix2x2 ( scalar mOO , scalar mOl , scalar mlO , scalar mll ) ; void Element ( int row , int col , scalar e lementValue ) ; scalar Element ( int row , int col ) ; matrix2x2 &operator = ( scalar initializationArray [ 2 ] [ 2 ] ) ; }; clas s matrix3x3 { private : scalar elements [ 3 ] [ 3 ] ; puЫic : matrix3x3 (void) ; matrix3x3 ( s calar matrix3x3 ( scalar scalar scalar

initializationArray [ 3 ] [ 3 ] ) ; mOO , scalar mOl , scalar m02 , mlO , scalar ml l , scalar m12 , m2 0 , scalar m2 1 , scalar m22 ) ;

void Element ( int row , int col , scalar elementValue ) ; scalar Element ( int row , int col ) ; matrix3x3 &operator = ( scalar initializationArray [ 3 ] [ 3 ] ) ; };

В этих классах, как и в ранних версиях классов векторов, есть только элементы данных и методы записи и чтения значений этих элементов. В нескольких последующих разделах мы добавим в них возможности выполнения различных операций над матрицами. Однако их код обычно весьма прост и в книге по большей части не приводится. На компакт-диске, поставляющемся с книгой, содержится полная версия библиотеки моде­ лирования, в которой реализованы все методы, выполняющие операции над матрицами . Вы найдете эту библиотеку в папке Source\Chap­ terOЗ \Vectors and Matrices, в файле PммathLibVl O . h.

83

М атем а тичес кие и нстр ум енты

Единичная матрица Одна из простейших операций , которые можно выполнить над матри­ цей, инициализация ее единичной матрицей. Единичная матрица (unit это квадратная матрица, все элементы которой равны О, кроме matrix) элементов, расположенных на диагонали, идущей от левого верхнего угла в правый нижний. Это описание легче понят:ь, если рассмотреть пример ниже . Единичная матрица размера 2 Х 2 выглядит следующим образом: -

-

I= [� �] Единичная матрица 3 х 3 имеет вид о

1

о

Как видите, в единичной матрице все единицы расположены на одной диа­ гонали (эта диагональ называется главной), а остальные элементы равны О. Код функций Identity ( ) , формирующих единичные матрицы, есть в файле с исходным кодом библиотеки PММathLibVl O . h на компакт-диске.

За м ечание Единич ную матрицу иногда называют матрицей идентичности (identity

matrix) .

Умножение матрицы на единичную матрицу дает ту же самую матри­ цу, то есть фактически ничего не делает: Al = IA = A

Сложение и вычитание матри ц Сложение матриц выполняется очень просто. Складывать друг с другом можно только матрицы одинакового размера. При этом складываются со­ ответствующие элементы матриц . Пусть у нас есть матрицы А и В, пока­ занные ниже:

Глава 3

84

Тогда результатом сложения этих матриц будет такая матрица:

Вычитание одной матрицы из другой выполняете.я аналогично:

В классах matrix2x2 и matrix3x3 реализованы операторы + , +=, - и -=. В классе matrix2x2 элементы матрицы прямо перечисляются в коде операторов, поскольку этих элементов всего четыре . Однако в классе matrixЗxЗ для перебора элементов и выполнения сложения использует­ е.я пара вложенных циклов. Хот.я это несколько менее эффективно, код получаете.я более понятным.

Умножение и деление матр и цы на скаля р Чтобы умножить матрицу на скаляр, нужно просто умножить на этот скаляр каждый элемент матрицы. Пусть у нас есть матрица А и скаляр w. Тогда, если

А= то

wA =

[ [

а 00 • 01 al O al l

]

wa 00 wa 01 wa1 0 wa1 1

]

В каждом из классов matrix2x2 и matrixЗxЗ есть по два операто­ ра, выполняющих умножение матрицы на скал.яр. Один из операторов выполняет умножение, если левым операндом являете.я матрица. Второй -

-

85

М атем атические инструменты

оператор реализован как дружественная функция, выполняющая умно­ жение, если левым операндом является скаляр. Эти операторы можно ис­ пользовать так: matrix2x2 ml , m2 ( 1 , 2 , З , 4 ) ; scalar s = 5 ; ml = m2 * s ;

Или то же самое можно записать в виде matrix2x2 ml , m2 ( 1 , 2 , З , 4 ) ; scalar s = 5 ; ml = s * m2 ;

И тот, и другой вариант будут работать. В классах также есть опера­ торы *=, позволяющие умножить матрицу на скаляр и сохранить резуль­ тат в той же матрице. Деление матрицы на скаляр выполняется примерно так же, как умно­ жение. Чтобы разделить матрицу на скаляр , нужно разделить на этот скал.яр каждый элемент матрицы, как показано ниже:

В отличие от умножения, деление не коммутативно, поэтому в клас­ сах matrix2x2 и matrixЗxЗ есть только по одному оператору / . Однако в классах есть операторы /=.

Перемножение матри ц Кроме умножения матрицы на скал.яр, одну матрицу можно умножить на другую, получив в результате новую матрицу. Однако есть ограниче­ ния на размеры перемножаемых матриц. Две матрицы можно перемно­ жить только в том случае, если количество столбцов в первой матрице равно количеству строк во второй. То есть, можно перемножить матрицы p x n и n x q, но нельзя перемножить матрицы p x n и q X n . Результатом пе­ ремножения матриц p x n и n x q будет матрица размера p x q (с р строками и q столбцами) .

З а м ечание Иногда перемножение матриц называют в программировании цией матриц.

конкатена ­

86

Глава 3

Вот пара примеров матриц, которые можно перемножать. Перемно­ жение матриц размерами 2 х 3 и 3 х 7 даст в результате матрицу 2 х 7. Пе­ ремножение матриц размерами 2 х 3 и 3 х 5 даст в результате матрицу 2 х 5 . Перемножить матрицы размерами 2 х 3 и 5 х 3 нельзя. Сам метод перемножения на первый взгляд может показаться запу­ танным, но со временем вы привыкнете к нему. Перемножение матриц можно рассматривать по-разному. Я предложу пару подходов, и вы смо­ жете решить, какой вам больше нравится. Первый подход - воспринимать перемножение матриц как последова­ тельности скалярных произведений векторов. Взгляните на строку пер­ вой матрицы. Она выглядит как набор компонентов вектора, правда? А теперь посмотрите на первый столбец второй матрицы. Он тоже выглядит как набор компонентов вектора. А теперь можно « перемножить» эти два вектора, чтобы получить скаляр. Этот скаляр будет значением на пересе­ чении первого столбца и первой строки результирующей матрицы. Вот пример того, как перемножаются матрицы . Предположим , что у нас есть матрица 3 Х 3 :

Умножим е е н а матрицу В размером 3 х 2 :

Результатом перемножения будет матрица 3 х 2 . Чтобы получить ее элемент (О, О), нужно перемножить первую строку матрицы А и первый столбец матрицы В:

( 2 4 3 ) • ( 1 2 4) = (2 ) 1

+

(4 ) 2 + (3 )4 = 2 + 8 + 1 2 = 22

Элемент ( О , О ) равен 2 2 . Результирующая матрица н а данный момент выглядит так:

87

М атем а тические и нструменты

Элемент (О, 1) - первая строка, второй столбец - получается в результа­ те перемножения первой строки матрицы А и второго столбца матрицы В:

(2 4 3) • (2 3 3)

=

(2)2 + (4)3 + (3)3

=

4 + 12 + 9 = 25

Теперь результирующая матрица выглядит как

Продолжим. Элемент ( 1 , 0) - вторая строка, первый столбец - получа­ ется в результате перемножения второй строки матрицы А и первого столбца матрицы В :

( 4 5 2) • ( 1 2 4 ) = (4)1 + (5)2 + (2)4

=

4 + 10 + 8

=

22

Теперь результирующая матрица будет выглядеть так:

r 1

Если мы продолжим перемножать элементы, мы получим следующий результат:

2 4 5 2 23 4 1 43

зlr1 1

=

22 25 22 25 1 6 23

Этот метод перемножения матриц просто запомнить, и он иллюстри­ рует математическую связь между матрицами и компонентами векторов. Второй метод, который я хочу продемонстрировать, - это просто фор­ мула. Основное преимущество этого метода в том, что его можно объяс­ нить одной строкой. Если А - матрица размером i x n , а В - матрица размером n x j , то верно следующее: АВ=С в том и только в том случае, если n-1

=� сIJ .. �

k=O

+ а.1ь1 a.kьk. 1 J = а1.оьо.з 1 J. + + а.( 1 n- 1)ь(n- 1 2·� •· ·

Знак � - это заглавная греческая буква сигма, обозначающая сумму.

88

Глава 3 Если в формуле есть обозначение n

I

k=l

оно означает сумму того, что находится справа от этого обозначения, при­ чем суммируются n элементов, в первом из которых k = l , во втором - k=2 и так далее до k =n. Например, 4

I2 = 2 + 2 + 2 + 2 = s k= l 4

:L k = l + 2 + 3 + 4 = 10

k=l

Используя формулу для матриц из предыдущего примера, мы получим:

3

с00 = L a0kbk0 = а00Ъ00 + а01Ъ10 + а02Ъ20 k=O

=

2• 1 + 4•2 + 3•4 = 2 + 8 + 1 2

= 22

Поскольку перемножение двух матриц - операция более замыслова­ тая, чем рассмотренные ранее операции с матрицами, рассмотрим код методов перемножения матриц. Этот код приведен в листинге 3 . 1 3 . Листинг 3 . 1 3 . Умножение матрицы н а матри цу 1 2 з 4 5 б

7 8

9 10 11 12 13

matrix2x2 matrix2x2 : : operator * (const matrix2x2 &rightOperand) return ( matrix2x2 ( // Значение элемента 0 , 0 elements [ O ] [ O ] * rightOperand . elements [ O ] elements [ O ] [ l ] *rightOperand . elements [ l ] / / Значение элемента 0 , 1 elements [ O ] [ O ] *rightOperand . elements [ O ] elements [ O ] [ l ] * rightOperand . elements [ l ] / / Значение элемента 1 , 0 elements [ l ] [ O ] * rightOperand . elements [ O ]

[О] + [О] , [1] + [1] , [О] +

М атем атическ и е и нструменты

89

elements [ l ] [ l ] *rightOperand . elements [ l ] [ О ] , 14 15 1 1 Значение элемента 1 , 1 elements [ l ] [ O ] * rightOperand . elements [ O ] [ 1 ] + 16 elements [ l ] [ l ] * rightOperand . elements [ l ] [ 1 ] ) ) ; 17 18 } 19 2 0 matrix3x3 matrix3x3 : : operator * (const matrix3x3 &rightOperand) 21 22 matrix3x3 answer ; 23 for ( int i=O ; i = 45°)

---+-+--+---- _.

SR

[ ] 4 0

S= 0 1

---+--+----

_.

S=

-------

-----1--• -----+-----1t-

Рис. 4. 1 2 . Преобразования

[ ] 4 0 01

_. --�--+----:ЭI...

R(E> = 45°) _.

-------

RS и SR

Как видите, поворот после масштабирования и поворот до масштаби­ рования приводят к абсолютно разным результатам . Так что в общем слу­ чае порядок преобразований имеет значение.

20- пр еоб разо ван ия и ре н де р и нг

1 09

Пр и менен ие п р еоб р азова н и й вра ща ющи и с я т реу гол ьник -

А теперь, чтобы увидеть, как все описанное применяется на практике,

используем платформу физического моделирования, рассмотренную в главе 2 « Имитация 3D-графики с помощью DirectX» , для создания простой программы. Она будет отображать на экране двумерный треуголь­ ник, вращая его с помощью заданных матриц преобразований.

При менение платформы физического модели рования Платформа физического моделирования облегчает процесс подготовки Direct3D к работе. Но это не значит, что все нужное будет сделано за вас. Вам придется создать проект для размещения программы в Visual Studio и настроить его конфигурацию. Затем нужно добавить код в функции платформы.

СОЗДАНИЕ ПРО Е КТА Если вы создаете и выполняете примеры программ, читая эту книгу (я рекомендую вам это делать), создайте проект для примера программы. Как это сделать, зависит от того, какую версию Visual Studio вы исполь­ зуете. Если, например, вы используете Visual Studio 7 , то для создания проекта выполните следующие действия: 1.

2. З. 4. 5. 6.

Если Visual Studio не запущен, запустите его. Вероятно, после запуска он отобразит стартовую страницу - Start Page. Если отображена стартовая страница, нажмите на ней кнопку New Project. Если нет, выберите в меню Fi]e пункт New и в от­ крывшемся подменю выберите пункт Project. Должно отобразиться диалоговое окно New Project. В списке Project Types в левой части этого окна выберите папку Visual С++ Projects. В списке Templates в правой части окна выберите пункт Win32 Project. Не выбирайте пункт DirectX 9 Visual С++ Wizard, если хотите использовать платформу физического моделирования. Задайте имя проекта и путь к папке, в которой о н должен нахо­ диться. Для нашего текущего проекта задайте имя TriSpin. Нажмите кнопку ОК. В окне мастера Win32 Application Wizard перейдите на вкладку Application Settings. На этой вкладке установите флажок Empty Proj ect. Если установить э•rот флажок, Visual

1 10

Глава 4 Studio создаст пустой проект, не загроможденный вспомогатель­ ными функциями, не нужньrми вашей программе. Нажмите кнопку Finish, и создание проекта закончено.

Когда мастер завершит работу , Visual Studio вернется к стартовой странице. Вы создали проект, но в нем нет исходных файлов программы. Добавим к проекту файлы платформы физического моделирования. 1.

2.

3.

Скопируйте с компакт-диска, распространяемого с книгой, файлы РМDЗDАрр . h и РМDЗDАрр . срр. Поместите их в папку только что созданного проекта. На компакт-диске эти файлы размещаются в папке Source\Chapter0 4 \TriSpin. Вернитесь в Visual Studio . В Solution Explorer щелкните пра­ вой кнопкой мыши на папке Source Files. Из появившегося контекстного меню выберите пункт Add. В открывшемся под­ меню выберите пункт Add Existing Item. Visual Studio отобразит диалоговое окно Add Existing Item. В нем выберите файл РМDЗDАрр . срр и нажмите кнопку Open .

Теперь платформа добавлена в проект. Если хотите, можете повто­ рить процедуру для файла РМDЗDАрр . h, щелкнув правой кнопкой на пап­ ке Header Files. Это не обязательно, но не повредит.

КОНФИГУРИРОВАН И Е ПРОЕКТА Чтобы сделать обычную Windows-пpoгpaммy приложением DirectX, нужно добавить в конфигурацию проекта некоторые дополнительные данные. Во-первых, нужно добавить сведения о папке, содержащей биб­ лиотечные файлы DirectX. 1.

В Solution Explorer подведите указатель к имени проекта. Если вы следуете указаниям этой книги, это имя TriSpin. Щелк­ ните правой кнопкой мыш:и. В появившемся контекстном меню выберите пункт Properties. -

2.

3.

Visual Studio отобразит диалоговое окно Property Pages. В спи­ ске в левой части этого окна выберите папку Linker и в рас­ крывшейся папке выберите пункт General . В правой части окна в списке свойств выберите пункт Additio­ nal Library Directories. Справа от надписи введите: С : \DXSDK\lib

Если DirectX SDK установлен не в папке С : \DXSDK, укажите папку, в которой он установлен, и ее подпапку lib. 4.

В списке в левой части окна выберите пункт Input в папке Linker.

111

20- пр е образован ия и ре н де р и н г 5.

Visual Studio отобразит другой список свойств. Одно из них на­ зывается Additional Dependencies. В поле справа от него введи­ те следующий текст: dinput8 . lib dЗdxof . lib dxguid . lib d3dx9dt . lib d3d9 . lib winпnn . lib kernel32 . lib user32 . lib gdi32 . lib winspool . lib comdlg32 . l ib advapi32 . lib shell3 2 . lib ole32 . lib oleaut32 . lib uuid . lib odЬc32 . lib odЬccp32 . lib

Щелкните на кнопке ОК. Можно избежать ручного ввода всего текста из пункта 5, скопировав этот текст из файла AdditionalDependencies . txt и вставив в поле Ad­ ditional Dependencies. Этот файл хранится на компакт-диске в папке So­ urce.

ДОБАВЛЕНИЕ НУЖНЫХ ФУНКЦИ Й Последний шаг в подготовке примера программы - добавление в проект файла, содержащего функции, необходимые платформе физического мо­ делирования: 1

.

2.

Скопируйте с компакт-диска, распространяемого с этой кни­ гой, в папку проекта файл FrameFns . срр . Вы найдете этот файл в папке Source. Переименуйте файл FrameFns срр соответственно имени про­ екта. Для этого примера можно назвать его TriSpin . срр . .

Теперь мы готовы приступить к написанию кода.

Настройка геометрии Объекты компьютерной графики, которые вы видите на экране, обычно описываются как совокупности точек. :Каждая точка описывается парой координат (х, у), если вы работаете с 2D-графикой. В 3D-графике для описания одной точки используются три координаты - (х, у, z). Точки, определяющие объекты, называются вертексами. Для нашего приложе­ ния, которое должно отображать треугольник, придется определить и треугольник, и его вертексы. В Direct3D используется формат вертексов, который Microsoft назы­ вает гибким форматом вертексов (flexiЫe vertex format). Этот формат позволяет создать вертекс почти любого типа (в определенных пределах), какой может понадобиться вашему приложению. Для простой формы, вроде треугольника, достаточно простейшего формата вертексов с каки­ ми-то компонентами и цветом. Более сложные форматы вертексов могут хранить информацию о нормалях, материале и текстуре.

Глава 4

1 12

П реоб р а зов анн ы е и не п реоб р а зов анн ы е в е ртексы Выполнять рендеринг в DirectЗD можно двумя способами . Можно вы пол­ н ить все преобразования самому и передать устройству DirectЗD вертек­ сы, которые нужно непосредственно выводить на экран . Есть и другой способ. Можно указать DirectЗD, какие преобразования нужно выпол н ить, и передать устройству н е п реобразованные вертексы объекта. DirectЗD вы­ пол н ит заданные вами преобразования и выведет на экран их результат. M icrosoft настаивала на том , что все игры должны работать с ЗD-графикой, и результатом такой настойчивости стало одно странное свойство: единст­ венный формат преобразованных вертексов это DЗDFVF_XYZRHW. У вер­ тексов этого формата есть четыре компонента - х, у, z и w. В этом примере у всех вертексов компоненту z будет присваи ваться значение О, а w значение 1 , чтобы мы могли сосредоточиться на 20-графике. -

-

В листинге 4 . 1 приведен формат вертексов, используемых в програм­ ме отображения: вращающегося: треугольника. Листинг 4. 1 . Структура вертексов для программы отображения

вращающегося треугол ьника 1 2 з 4 5 6

s truct vertex float х , у , z ; DWORD color ; };

11 Непреобразованная позиция 11 вертекса в ЗD-координатах 11 Цвет вертекса

Тип вертекса, определенный в листинге 4 . 1 , содержит элементы дл.я: хранения: координат х, у и z, определяющих позицию вертекса. :Кроме того , в типе есть элемент для хранения цвета вертекса. Отображая фигу­ ры с помощью вертексов этого типа, Direct3D будет смешивать цвета разных вертексов. Например, как мы скоро увидим, в программе ото­ бражения вращающегося треугольника каждому вертексу треугольника задан свой цвет. Один из этих цветов - красный, а второй - синий. Для каждого пикселя треугольника между этими вертексами Direct3D опре­ делит цвет, смешивая цвета двух вертексов. Пиксели, расположенные ближе к красному вертексу, будут более красного оттенка, а расположен­ ные ближе к синему - более синего. Чтобы Direct3D смешивал цвета вертексов, программа должна объяс­ нить ему, что содержится в каждом вертексе. Это можно сделать с помо­ щью набора флагов гибкого формата вертексов. Обозначения всех флагов начинаются с символов DЗDFVF_, за которыми следует описание формата вертекса. В программе отображения вращающегося треугольника в вер­ тексах хранятся их координаты (х, у, z) и цвет. Об этом и должны сооб­ щить Direct3D флаги. В программе используются: флаги DЗDFVF_XYZ и

20- пр еоб р азо ван ия и р е н де р и нг

113

DЗDFVF_DIFFUSE. За дополнительной информацией о флагах обратитесь к теме D3DFVF в документации по DirectX 9.0.

Чтобы упростить работу с флагами, в программе определена констан­ та, сочетающая их. Ее определение выглядит так: #define VERTEX_TYPE_SPECIFIER ( DЗDFVF_XYZ / D3DFVF_DI FFUSE )

Определение формата вертексов и флаги используются программой в функции Gameini tiali z a tion ( ) . Эта функция находится в файле TriSpin . cpp (в папке Source\Chapter0 4 \ TriSpin на компакт-диске, поставляющемся с книгой). Листин г 4 . 2 . Функция Gamelnitialization() 1 2 3 4 5 6 7

8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

bool Gameinitiali zation ( ) { 1 1 Инициализируем три vertex theVerteces [ ] { { -1 . 0f , -1 . 0f, { l . Of , - 1 . 0 f , l . Of , { 0 . 0f , };

вертекса для треугольника .

=

O . O f , OxffffOO O O , } , O . O f , OxffOO OOff , } , O . O f , Oxffffffff , } ,

LPDIRECT3DVERTEXВUFFER9 tempPointer NULL ; 11 Создаем вертексный буфер . 11 Е сли его не удалось создать . . . if ( FAILED ( theApp . D3DRenderingDevice ( ) ->CreateVertexВuffer ( 3 * sizeof (vertex) , О , VERTEX_TYPE_SPECIFIER , D3DPOOL_DEFAULT , &tempPointer , NULL) ) ) =

1 1 Пример нельзя выполнить . return false ; else 11 Сохраняем указатель на вертексный буфер в 11 глобальной переменной приложения . theApp . D3DVertexВuffer ( tempPointer ) ; } 11 1 1 Заполняем вертексный буфер .

11

VOID* tempBufferPointer ;

1 14

Глава 4 1 1 Блокируем доступ к нему .

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

if ( FAILED ( theApp . D3DVertexВuffer ( ) - >Lock ( О , 3 * s i zeof (vertex) , (void* * ) & tempBufferPointer , O ) ) ) return false ; }

1 1 Копируем вертексы в буфер . memcpy ( tempBufferPointer , theVerteces , З*sizeof (vertex) ) ; 1 1 Открываем буфер . theApp . D3DVertexВuffer ( ) ->Unlock ( ) ; return ( true ) ;

50

Когда программа отображения вращающегося треугольника загружает­ ся, платформа вызывает функцию Gameinitialization ( ) . Эта функция начинается с объявления массива для хранения трех вертексов (вершин) треугольника. При определении инициализируются координаты и цвет каждого вертекса. Код определения приведен в строках 4-9 в листинге 4.2. Зам ечание Использование статических массивов для хранения вертексов - не самый эффективный м етод использования памяти. Можно испол ьзовать динами­ чески выделяемые массивы, но их использование усложняет алгоритмы . В нашей программе м ы вы водим н а экран только один 20-треугольник. Объем используемой памяти незначителе н , и для упрощения программы в ней используется статический массив . В и грах, в которых нужно быстро обрабатывать данные, часто испол ьзуются статические массивы для хра­ нения небольших объемов данных.

Мы можем использовать множество операций, которые способен вы­ полнять Direct3D над вертексными буферами. Вертексный буфер - это область в системной памяти или видеопамяти, которая используется для пакетной обработки вертексов. Идея состоит в том, чтобы заполнить вер­ тексный буфер вертексами и вызвать функцию, которая выполнит ка­ кие-то действия над ними - переместит, повернет или выведет на экран. Вертексный буфер в Direct3D представляет собой СОМ-объект. Исполь­ зуя этот буфер, ваша программа должна выполнять стандартную проце­ дуру создания СОМ-объекта: 1. 2.

Создать переменную для хранения указателя на интерфейс объекта и присвоить ей значение NULL. Вызвать функцию для создания вертексного буфера.

20-пр е об разования и р е нде ри нг

115

Программа создает вертексный буфер, вызывая функцию Direct3D CreateVerteXВuffer ( ) в строках 1 4 - 1 8 листинга 4 . 2 . Заметьте, что при вызове функции Crea teVertexBuffer ( ) используется константа VERTEX_TYPE_SPECIFIER, определенная ранее в программе. Если создание вертексного буфера прошло успешно, указатель на него сохраняется в переменной в классе dЗd_app в строке 27. Затем вер­ тексный буфер заполняется . Чтобы заполнить его, сначала нужно забло­ кировать доступ к нему извне, чтобы к нему могла обращаться только эта программа. Блокирование доступа к буферу необходимо при выполнении большинства операций над этим буфером. Если заблокировать доступ к буферу удалось, то функция Gameini­ tial ization ( ) копирует в него вертексы в строке 43 листинга 4 . 2 . По­ сле этого выполняется разблокирование буфера.

Обновл ение кадров После того, как программа выполнила инициализацию геометрической фигуры (треугольника), которую нужно отобразить на экране, начинает­ ся обработка поступающих сообщений. Кроме того, начинается обновле­ ние кадров и вывод их на экран. Программа отображения вращающегося треугольника настолько проста, что в ней не нужно обрабатывать ка­ кие-то сообщения, кроме тех, что уже обрабатывает платформа. Однако обновлять кадры необходимо. Эту операцию выполняет функция Upda­ teFrame ( ) , код которой приведен в листинге 4 . 3 . Листинг 4 . 3 . Функция U pdateFrame( ) 1 bool UpdateFrame ( ) 2 { /* Глобальная матрица будет просто :вращать объект вокруг 3 4 начала координат :в плоскости ху . * / DЗDXМATRIXAl б worldМatrix; 5 / / Создадим матрицу вращения , чтобы выполнять 1 полный 6 // оборот ( 2 * PI радианов ) за 1 0 0 0 мс . Во избежание потери 7 / / точности , свойственной очень большим числам с ппа:вапцей 8 9 // запятой, :вычисляется остаток от деления на 1000 системного / / :времени , и этот остаток преобразуется :в угол в радианах . 10 timeGetтime ( ) % 1 0 0 0 ; 11 UINT currentTime FLOAT rotationAngle currentTime * (2 . 0f * DЗDX_PI ) / 1000 . 0f ; 12 DЗDXМatrixRotationZ ( &worldМatrix , rotationAngle ) ; 13 theApp . DЗDRenderingDevice ( ) ->SetTransform (DЗDTS_WORLD , 14 15 &worldМatrix) ; 16 / / Создадим матрицу отображения . Дпя ее определения 17 / / используется позиция набmодателя , точка , в которую 18 / / направлен его :взгляд , и направление , указы:вапцее , где 19 / / находится :верх . В этом примере наблюдатель находится :в 5 20 21 // единицах сзади п о оси z и в 3 единицах сверху , смотрит =

=

Глава 4

1 16 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

/ / н а начало координат , а ":верхом" считается направление / / роста координаты у D3DXVECTOR3 eyePoint ( O . Of , 3 . 0f , - 5 . 0f ) ; D3DXVECTOR3 lookatPoint ( O . Of , O . Of , O . O f) ; D3DXVECTOR3 upDirection ( O . Of , 1 . 0f , O . O f ) ; D3DXМATRIXA1 6 viewмatrix ; D3DxмatrixLookAtLН ( &viewМatrix , &eyePoint, &lookatPoint , &upDirection) ; theApp . D3DRenderingDevice ( ) ->SetTransform (D3DTS_VIEW, &viewмatrix) ; // Матрица проецирования в этом примере выполняет //преобразование // перспекти:вы , преобразупцее геометрию из 3D-представпения в // плоское 2D-предста:впение . Она содержит депите.пь перспеК'I'ИВЫ , / / который уменьшает видимый размер далеких объектов . Чтобы // создать перспективное преобразование , нужно задать попе / / зрения (обычно 1 / 4 PI ) , соотношение перспективы , бnИЖНЮIО // и дапьнпо плоскости отсечения . Плоскости опредеnяют // предельные расстояния , за которыми объекты не обрабаты:ваются . D3DXМATRIXA1 6 proj ectionмatrix ; D3DxмatrixPerspectiveFovLH ( &projectionмatrix , D3DX_PI /4 , 1 . 0 f , 1 . 0f , 1 0 0 . 0 f ) ; theApp . D3DRenderingDevice ( ) ->SetTransform (D3DTS_PROJECTION , &projectionмatrix) ; return ( true ) ;

При каждом вызове функция UpdateFrame ( ) устанавливает угол по­ ворота. Используя этот угол, она строит матрицу вращения, как показано в строке 13 листинга 4 . 3 . Затем эта функция делает созданную матрицу глобальной матрицей преобразования (в строке 14 листинга 4. 3). Что такое глобальная матрица преобразования? В 3D-программировании наблюдатель не перемещается в моделируе­ мом мире. Вместо этого мир перемещается вокруг наблюдателя. Чтобы переместить наблюдателя вперед, программа сдвигает мир назад. А какое это имеет отношение к треугольнику? В программе TriSpin для создания матрицы вращения, поворачиваю­ щей треугольник в плоскости ху, вызывается функция Direct3D DЗDмat­ rixRotationZ ( ) . Эта матрица сохраняется в глобальной матрице. Затем Direct3D использует глобальную матрицу для поворота всех вертексов в отображаемом программой мире. В мире программы TriSpin есть только три вертекса - это вершины треугольника. Соответственно, весь мир (один треугольник) будет вращаться в плоскости ху. Кроме глобальной матрицы , функция UpdateFrame ( ) создает матри­ цу отображения и матрицу проецирования. Матрица отображения задает позицию наблюдателя в 3D-мире. Чтобы создать эту матрицу, программа

20- преобразования и ре ндери н г

1 17

использует функцию Direct3D DЗDмatrixLookAtLH ( ) . Эта функция со­ здает матрицу отображения в левосторонней системе координат, исполь­ зуемой в Direct3D . Матрица проецирования добавляет перспективу в отображаемый мир, поэтому более далекие объекты будут казаться меньше по размеру. Поскольку в этой главе мы работаем с 2D-графикой, нам нет нужды за­ ботиться о перспективе, однако создать матрицу проецирования все же придется . Это можно сделать, вызвав функцию Direct3D DЗDMatrix­ PerspectiveFovLH ( ) .

Зам е чание В л исти нгах, приводимых в данной книге, некоторые комментарии , п р и ­ сутствующие в файлах на ком пакт-диске , удалены , чтобы сэкономить место . В файл ах на компакт-диске комментариев довольно м ного. Если вам нужна дополнительная и нформация о функциях, упомянутых в книге , обратитесь к файлам на компакт-диске и л и к документаци и по DirectX.

Рендеринг кадров Чтобы выполнить рендеринг треугольника, платформа вызывает функ­ цию RenderFrame ( ) . Чтобы вывести треугольник на экран, эта функция должна выполнить три действия: 1.

2.

3.

Задать Direct3D поток-источник для рендеринга. Задать формат вертексов. Выполнить рендеринг треугольника.

Код функции RenderFrame ( ) приведен в листинге 4 . 4 . Листинг 4. 4. Функция RenderFrame( ) 1 bool RenderFrame ( ) 2 { З // Реидеринг содержимого вертексного буфера theApp . DЗDRenderingDevice ( ) 4 ->SetStreamSource ( O , theApp . DЗDVertexBuffer ( ) , 5 О , s i zeof (vertex) ) ; 6 theApp . D ЗDRenderingDevice ( ) - >SetFVF (VERTEX_TYPE_SPECIFIER) ; 7 theApp . DЗDRenderingDevice ( ) -> 8 DrawPrimi tive (DЗDРТ ТRIANGLESTRIP , О , 1) ; 9 10 return ( true) ; 11 12 _

Для вывода содержимого вертексных буферов Direct3D использует пото­ ки рендеринга. В строках 4-6 листинга 4.4 созданный функцией Game

1 18

Глава 4

Ini tialization ( ) вертексный буфер выбирается в качестве источника по­ тока рендеринга. Чтобы обрабатывать вертексы из этого потока, Direct3D должен знать формат этих вертексов. Этот формат задается в строке 7. В строке 8 листинга 4 . 4 вызывается функция Direct3D DrawPrimiti­ ve ( ) , выполняющая собственно рисование. Она выводит треугольник как группу соединенных треугольников. Вывод объектов в виде последова­ тельностей треугольников - это обычный способ их отображения. Вертексы большинства 3D-объектов обычно являются вершинами треугольников, а сами 3D-объекты воспринимаются как совокупности треугольников.

З а пуск программы Чтобы скомпилировать и запустить программу, выполните следующие действия: 1 Создайте проект для нее, как описано ранее в разделе « Созда­ ние проекта» этой главы. .

2. З.

4.

Сконфигурируйте проект, как описано ранее в разделе « Кон­ фигурирование проекта» этой главы. Скопируйте файлы программы с компакт-диска в папку только что созданного проекта. На компакт-диске эти файлы находятся в папке Source \Chapter04\TriSpin. Вам понадобятся файлы TriSpin . cpp, PМDЗDApp . h и РМDЗDАрр . срр . В Visual Studio щелкните правой кнопкой мыши на папке So­ urce Files в Solution Explorer. Из появившегося контекстно­ го меню выберите пункт Add, а из открывшегося подменю Add Existing Item. Добавьте в проект файлы срр, скопирован­ ные в шаге 3 . .

5.

Нажмите клавишу F5, чтобы скомпилировать и запустить про­ грамму.

Если вы просто хотите увидеть программу в работе, запустите файл TriSpin . exe из папки Source\Chapter04 \TriSpin на компакт-диске.

И то г и В этой главе мы далеко продвинулись. Начав с элементарных понятий о матрицах и векторах, мы разобрались, как выполнять перемещения, пово­ роты и масштабирования. Сочетая эти преобразования, мы смогли выпол­ нять повороты и масштабирования относительно произвольных точек. Мы создали и вывели на экран простой объект. Применяя преобразо­ вания, мы заставили его двигаться, вращаясь в окне. В следующей главе мы увидим, как сделать то же самое в трехмерном пространстве.

Гла ва 5

З D - п рео бра зов а н и я и ре н де ри н г В этой главе мы перейдем от работы на двумерных плоскостях к работе в трехмерных пространствах . Мы разберемся, как перенести все преобразо­ вания из четвертой главы « 2D-преобразования и рендеринг>} в трехмер­ ное пространство. По ряду причин эти преобразования в трехмерных пространствах более сложны. Одна из основных причин - в трехмерных сценах должна присутствовать перспектива, и объекты, более удаленные от наблюдателя, должны казаться меньше по размеру, чем такие же, но более близкие объекты. В процессе чтения этой главы вы узнаете, как перспектива влияет на рендеринг. В этой главе также показано, как создавать 3D-модели объектов в глобальных координатных системах и отображать их. В примерах из этой главы мы будем использовать математические преобразования, рас­ смотренные в предыдущих главах .

ЗD- п реоб р азова н и я С математической точки зрения обобщить преобразования в трехмерные системы координат не слишком сложно . Вспомните, что с помощью век­ тора можно задать любую точку в системе координат . Поэтому, если гово­ рить о математике, переход от 2D к 3D сводится просто к добавлению еще одного измерения к векторам и матрицам. Есть только одна маленькая особенность. Чтобы операции перемно­ жения векторов и матриц выполнялись корректно, нужно использовать однородные координаты.

Однородные координаты Однородные координаты добавляют дополнительное измерение к точкам , векторам и матрицам, используемым в ваших играх. В результате про­ грамме придется выполнять дополнительные вычисления . Так зачем же использовать эти координаты?

Гл ава 5

1 20

Если ваша игра использует однородные координаты, она может умно­ жать векторы на матрицы преобразований стандартным способом. Кроме того, можно будет объединять преобразования, преобразовывая их мат­ рицы в одну с помощью перемножения. Чтобы использовать однородные координаты в 3D, нужно добавить к каждому вертексу в программе еще один координатный компонент. При этом точки будут определяться не как (х, у, z), а как (х, у, z, w). Компонент w - это однородная координата w. Его единственное назначение - сделать возможным представление преобразований в виде матриц. Мы будем присва­ ивать этому компоненту значение 1 . Ему можно присваивать и другие значе­ ния, но для работы над нашей книгой в этом нет необходимости. Значит ли это, что нужно добавить еще один компонент со значением 1 к координатам каждого объекта? К счастью, нет. Direct3D делает присутствие четвертой координаты практически незаметным для программиста. Для вертексов задаются ко­ ординаты х, у и z , и Direct3D предоставляет функции для построения матриц преобразований, основанных на однородных координатах . Если вы приказываете Direct3D выполнить преобразование, он автоматически преобразует координаты х, у, z в х, у, z, w и перемножает их с матрицей преобразования. Но если Direct3D делает все за нас, то зачем вообще говорить о четвер­ той координате? В следующем разделе вы узнаете, как выполняются математические преобразования в трехмерных пространствах. Эти математические преоб­ разования требуют применения однородных координат.

Перемеще н ие Перемещение вектора в трехмерных координатах выполняется практиче­ ски так же, как и в двумерных - сложением вектора с вектором смеще­ ния t, представляющим перемещение. х' = х + t

Если вернуться к главе 4, вы увидите, что в двумерных координатах для перемещения используется в точности такое же уравнение. Единст­ венное отличие заключается в том, что вместо двумерных векторов те­ перь мы будем использовать трехмерные. Мы будем использовать однородную координату w в 3D-преобразова­ ниях - каждый вектор будет состоять из 4 компонентов, а каждая матри­ ца преобразования будет иметь размер 4 х 4 : о

о

1

о

Лу

Лz

о

1

о

о

о

121

ЗD- преобра зо ван ия и рендеринг

Заметьте - это матрица 4 х 4, а не 3 х 3 . Дополнительная строка и стол­ бец нужны, чтобы учесть однородную координату ·vv. Первый, второй и третий элементы слева в нижней строке содержат компоненты х, у и z век­ тора перемещения. Это величины смещений соответственно вдоль осей х, у и z, которые будут применяться к одному или нескольким вертексам . Чтобы переместить объект в трехмерных координатах, игра умножа­ ет каждый вертекс этого объекта на матрицу перемещения . Это делается так же, как и в двумерных координатах, но в трехмерных координатах используются вертексы с 4 компонентами, а не с 2, и матрицы преобразо­ ваний размером 4 х 4, а не 2 х 2 . Обратное преобразование перемещает вертекс в обратном направлении:

т- 1 =

1

о

о

о

о

1

1

о

-Лх

о

о

о

-Лz

-Лу

о

М асштабирование Обобщить операцию масштабирования для трехмерных координат не­ сложно. Если мы не используем однородные координаты и масштабиру­ ем объект с одинаковым коэффициентом по всем осям (то есть не сжимаем его вдоль одной оси , одновременно растягивая вдоль другой), можно по-прежнему использовать скаляр:

х' = sx Матричную форму тоже довольно просто обобщить. К коэффициентам масштабирования вдоль осей х и у - sx и sy - добавится третий коэффици­ ент для масштабирования по оси z sz : -

о

Если используются однородные координаты, матрица будет выгля­ деть так :

S=

о

0 о

о 0

о

о

о

S2

о

о

о

0 1

Глава 5

1 22

Матрица обратного преобразования будет выглядеть следующим об­ разом: 1 о

о 1

о

о

о

о

о

о

о

о

1 о

о 1

Вращение Обобщить вращение для случая трехмерных координат несколько слож­ нее, чем перемещение и масштабирование. Вместо единственного способа вращения вокруг начала координат в трехмерном пространстве есть бес­ конечное количество таких способов, каждый из которых соответствует определенной оси вращения (см. рис. 5. 1 ) . у

х

z

Рис. 5 . 1 . Бесконечное количество осей вращения, проходящих через начало координат в трехмерном пространстве

Как выясняется, все вращения можно представить в виде суммы вра­ щений по трем координатным осям. В этом можно убедиться, посмотрев на рисунок 5 . 2 , в котором показаны три оси вращения самолета - крен, тангаж и рыскание. Нам подойдут не любые три оси (для знатоков мате­ матики поясню - эти три оси должны быть линейно независимыми), но все равно есть бесконечное количество сочетаний этих трех осей. Это очень удобно. Благодаря этой особенности мы можем выбрать лю­ бые линейно независимые оси, которые сочтем подходящими . Разумеется, если предоставить компьютеру право решать, как именно что-то сделать, ничего хорошего не получится. Поэтому выберем для рабо­ ты привычный набор осей - оси х, у и z , как показано на рисунке 5 . 3 .

1 23

30-пр еобразо ван ия и ре нде р инг П редуп реждение

Положительные направления вращения вокруг осей зависят от коорди нат­ ной систе м ы . В левосторонней системе координат положительным счита­ ется направление вращения по часовой стрелке, а в правосторонней против часовой стрел ки . Рыскан ие

Рис. 5 . 2 . Крен , тангаж и

рыскание самолета

Оси на рисунке 5 . 3 формируют правостороннюю систему координат. у

х

z

Рис. 5 . 3 . Оси вращения х, у и z

Выполняя вращение на плоскости, мы практически выполняли вра­ щение вокруг оси z, как показано на рисунке 5 . 4 , поэтому матрицу вра­ щения вокруг оси z можно получить, просто дополнив матрицу для двумерного вращения:

R

z

=

cos(8 ) sin(8 ) -sin(8 ) cos(8 ) о о

о о

О

о

1

о

О о

о 1

Глава 5

1 24

Все матрицы вращения здесь будут содержать координату w. Если вам нужны матрицы вращения без этой координаты, просто избавьтесь от нижней строки и правого столбца. Две другие матрицы вращения можно получить с помощью тригоно­ метрии точно так же , как и первую. у

х

z

Рис. 5.4. Вращение на плоскости - это то же самое, что вращение в пространстве вокруг оси z

Для вращения вокруг оси х служит такая матрица: 1



=

о

о

cos(q>) sin(q> ) - sin(q>) cos(q>)

о о

о

о

о

о о о 1

Матрица для вращения вокруг оси у выглядит так :

R

у

=

cos(a)

о

-sin(a)

sin(a )

о

cos(a)

о о

1

о

о

о

о

о

1

о

Выбор букв е, ip и а для обозначения углов поворота случаен. Они про­ сто напоминают, что углы поворотов по каждой из трех осей (и соответст­ вующие матрицы вращения) никак не связаны друг с другом. Произвольный поворот можно представить в виде сочетания опреде­ ленных поворотов вокруг трех осей координат точно так же, как любое положение самолета есть результат определенных крена, рыскания и тангажа. Чтобы упростить нам работу, повороты всегда будут выполнять­ ся в последовательности х, у, z то есть первым будет вращение вокруг -

ЗD- п реоб разования и р е нде р и н г

1 25

оси х, вторым - вокруг оси у, а третьим - вокруг оси z . Хотя это простое правило тоже ничем не обусловлено, следуя ему, мы сможем четко и упо­ рядоченно выполнять вращения.

ОБРАТНЫЕ ВРАЩЕНИЯ Вы, вероятно, будете рады узнать, что матрицы: вращения в трехмерных системах координат (да и вообще в системах координат с любым количе­ ством изменений) обращаются транспонированц:ем. Поэтому записать обратные матрицы несложно:

1 R z-

=

cos(e )

- sin(e )

о

о

sin(0 )

cos(0 )

о

о

о

о 1

1 - о R-х о о

1 R -у

_

-

о

о

1

�11J

о

о

о

1

- sin(CreateVerteXВuffer ( 18 19 TOTAL_VERTICES * sizeof (vertex) , O , VERTEX_TYPE_SPECIFIER , 20 DЗDPOOL_DEFAULT , &tempPointer , NULL) ) ) 21 22 return false ; 23 24 else 25 26 theApp . D3DVerteXВuffer ( tempPointer ) ; 27 28 29 / / Заполняем вертехсный буфер . 30 VOID* tempBufferPointer ; 31 if (FAILED ( theApp . D3DVertexBuffer ( ) ->Lock ( 32 33 о, TOTAL_VERTICES * s izeof (vertex) , 34 (void* * ) &tempBufferPointer , 0 ) ) ) 35

1 34 36 37 38

Глава 5

return false ; memcpy (

39 40

tempBufferPointer , theVerteces ,

41 42

TOTAL_VERTICES*sizeof (vertex) ) ;

43

theApp . D3DVertexBuffer ( ) ->Unlock ( ) ;

44 45

return

( true ) ;

46

Как и в предыдущих версиях нашей программы, в примере с вращаю­ щейся пирамидой используется набор треугольников. Определены 6 вер­ шин, определяющих треугольники, из которых составлена пирамида. Обратите внимание, что одинаковы вертексы 1 и 5 и вертексы 2 и 6. Это сде­ лано для того, чтобы отрисовывались все стороны пирамиды. Вертексы 1 , 2 и 3 образуют первую сторону. Вторая сторона образуется вертексами 2, 3 и 4, третья - вертексами 3, 4 и 5, а четвертая - вертексами 4, 5 и 6. Определив вертексы, в строках 16-21 функция Gameinitial izati­ on ( ) создает вертексный буфер и сохраняет указатель на него в объекте dЗd_app . Затем в строках 3 1 -43 функция заполняет вертексный буфер .

ОБНОВЛЕН ИЕ КАД РОВ Изменения в UpdateFrame () вносить не обязательно, хотя в программе на компакт-диске вращение пирамиды и движение позиции наблюдения замедлено. В листинге 5 . 3 приведена новая функция UpdateFrame ( ) . Листинг 5 . 3 . Незнач ител ьные изменения в функции UpdateFrame( ) 1

bool UpdateFrame ( )

2

{

3

D3DXМATRIXA1 6 worldМatrix ;

4 5

D3DXМATRIXA1 6 ro tationY ;

6

D3DXМATRIXA1 6 rotationZ ;

7 8 9

D3DXМATRIXA1 6 rotationX ;

10

UINT currentTime timeGetTime ( ) % 4 0 0 0 ; FLOAT rotationAngle currentTime * ( 2 . 0f * D3DX_PI ) D3DXМatrixRo tationX ( &ro tationX , rotationAngle ) ;

11

DЗDXМatrixRo tationY ( &rotationY , ro tationAngl e ) ;

12

DЗDXМatrixRo tationZ ( &rotationZ , rotationAngle ) ;

13

D3DXМatrixМultiply ( &worldМatrix , &rotationX , &rotationY) ;

14

DЗDXМatrixМultiply ( &worldМatrix , &worldМatrix , &rotationZ ) ;

15 16

theApp . D3DRenderingDevice ( ) ->

17

=

=

SetTransform (D3DTS_WORLD , &worldМatrix ) ;

/ 4 000 . 0f ;

30-п реобразо ва ния и рендерин г 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

1 35

D3DXVECTOR3 eyePoint ( O . O f , 3 . 0 f , - 5 . 0 f ) ; D3DXVECTOR3 lookatPoint ( O . O f , O . Of , 0 . 0 f ) ; D3DXVECTOR3 upDirection ( O . O f , 1 . 0 f , 0 . 0 f ) ; D3DXМATRIXA1 6 viewМatrix ; D3DxмatrixLookAtLH ( &viewМatrix , &eyePoin t , &lookatPoin t , &upDirection) ; theApp . D3DRenderingDevice ( ) - > SetTransform (D3DTS_VIEW , &viewМatrix ) ; D3DXМATRIXA1 6 pro j ectionМatrix ; D3DxмatrixPerspectiveFovLН ( &proj ectionМatri x , D3DX_PI/4 , 1 . 0 f , 1 . 0 f , 1 00 . 0f ) ; theApp . D3DRenderingDevi ce ( ) ->SetTransform (D3DTS_PROJECTION , &projectionМatrix) ; return ( true ) ;

РЕНД ЕРИНГ КАД РА В функцию RenderFrame ( ) внесено одно, но весьма существенное изме­ нение. Сначала посмотрите на листинг 5.4, а потом мы разберем суть дан­ ного изменения. Листинг 5 . 4 . Рисование треугольной пирамиды 1 bool RenderFrame ( ) 2 { // Рендеринг содержимого вертексного буфера 3 theApp . D3DRenderingDevice ( ) ->SetS treamSource ( 4 5 о, theApp . D3DVerteXВuffer ( ) , 6 7 о, sizeof (vertex) ) ; 8 theApp . D3DRenderingDevice ( ) - >SetFVF (VERTEX_TYPE_SPECIFIER) ; 9 theApp . D3DRenderingDevice ( ) - >DrawPrimitive ( 10 D3DPT_TRIANGLESTRIP , 0 , 4 ) ; 11 12 return ( true) ; 13 14 }

Посмотрите на строку 1 1 в листинге 5.4. Вы увидите, что последний передаваемый функции DrawPrimitive ( ) параметр теперь равен 4 . Вы­ полняя рисование с помощью этой функции , программа должна сообщить ей, сколько треугольников нужно нарисовать. В примере с вращающимся треугольником нужно было нарисовать только один треугольник. В этом примере нужно нарисовать четыре треугольника, образующих пирамиду.

1 36

Глава 5

ОТСЕЧЕНИЕ НЕВ И Д ИМЫ Х ПОВЕРХНОСТЕ Й В предыдущих примерах программ отображался только один треуголь­ ник. Чтобы были видны обе его стороны, в функции InitDЗD ( ) в файле РМDЗDАрр . срр было отключено отсечение невидимых поверхностей. Если ваша программа отображает не пустотелые объекты, например, тре­ угольную пирамиду, вам не нужно отображать обе поверхности каждого треугольника. Вместо этого должна отображаться только поверхность треугольника, обращенная к вам. Поэтому функция Ini tDЗD ( ) измене­ на так, чтобы отсечение невидимых поверхностей не отключалось. Новая версия кода функции приведена в листинге 5 . 5 . Листинг 5 . 5 . Функция l n itDЗD( ) , не откл ючающая отсечение

невиди мых поверхностей l НRESULT InitD3D ( HWND hWnd) 2 { НRESULT hr = S_OK ; 3 4 D3DPRESENT PARAМETERS d3dpp ; 5 6 / / Создаем объект D3D . 7 if ( ( theApp . direct3D = Direct3DCreate9 (D3D_SDK_VERSION) ) NULL ) 8 { 9 // Если объект создать не удалось . . . 10 hr E_FAI L ; 11 12 else 13 14 / / Если объект D3D успешно создан . . . 15 / / Подготавливаем структуру , используеиую при создании 16 / / устройства D3DDevice 17 ZeroMemory ( & d3dpp , sizeof ( d3dpp) ) ; d3dpp . Windowed = TRUE ; 18 19 d3dpp . SwapEffect = D3DSWAPEFFECT_DISCARD ; d3dpp . BackBufferFormat = D3DFМТ_UNКNOWN ; 20 21 22 23 / / Создаем устройство D3DDevice 24 // Может ли устройство использовать НАL? if ( (hr==S_OK) && 25 26 (FAILED ( theApp . direct3D- >CreateDevice ( 27 D3DADAPTER_DEFAULT , D3DDEVТYPE_НAL , hWnd, 28 D3DCREATE_НARDWARE_VERTEXPROCESSING , &d3dpp , & theApp . d3dDevice ) ) ) ) 29 30 31 / / Если нет , возможно , удастся использовать 32 / / програниную эмуляцию . . . 33 if ( FAILED ( theApp . direct3D->CreateDevice (

30-п реобр азова ния и рендерин г D3DADAPTER_DEFAULT , D3DDEVТYPE_REF , hWnd , D3DCREATE_НARDWARE_VERTEXPROCESSING , &d3dpp , &theApp . d3dDevice) ) )

34 35 36 37 за

39 40 41 41 43 44 45 46 47 48 49 50 51 52 53 54 }

1 37

/ / Если нет , ув:ы hr = E_FAIL ;

.





if (hr=S_OK) / * В:ыХЛJОчаеи расчет освещения D3D , поскольку НЪ1 задаеи собственные цвета для вертексов . * / theApp . d3dDevice->SetRenderState (D3DRS_LIGHTING , FALSE ) ; }

return hr ;

Если вы посмотрите на строки 46- 5 1 , то увидите, что оператор, от­ ключавший отсечение невидимых поверхностей, удален. По умолчанию отсечение включено.

И то г и В этой главе рассматривались 3D-преобразования , 3D-конвейер и ренде­ ринг нескольких простых моделей. Мы затронули множество тем - неко­ торые из них мы рассмотрели подробно, некоторые - только кратко упомянули. Мы почти готовы заниматься моделированием физики, но сначала нам нужно еще кое-что сделать. В играх используются не простые моде­ ли , рассмотренные до сих пор. В них используются сложные объекты, описываемые 3D-сетчатыми моделями. Поэтому в следующей главе вы узнаете, как загружать и отображать сетчатые модели.

Гла в а 6

С етч а тые м одел и и Х - фа й л ы Эта глава названа « Сетчатые модели и Х-файлы » , но на самом деле она посвящена созданию моделей, выглядящих приемлемо. Моделирование физики в играх стало важным, когда достижения компьютерной графи­ ки позволили играм реалистично изображать объекты . Реалистичные объекты должны реалистично двигаться . Вам вряд ли удастся создать множество реалистичных объектов, впи­ сывая их прямо в программы в виде нескольких строк кода на С++. Вмес­ то этого объекты создаются в 3D-редакторах, например , MilkShape3D фирмы chUmbaLum sOft, 3ds max фирмы Discreet или trueSpace фирмы Caligari. Затем созданные объекты преобразуются в формат, который можно использовать в игре. Модели состоят из треугольников, поскольку треугольник - самая простая из возможных фигур на плоскости. Каждый треугольник образу­ ется тремя вертексами. Все три вершины обязательно лежат в одной плоскости. Часть плоскости между вершинами (вертексами) треугольни­ ка называется поверхностью (face) этого треугольника. В DirectX описан формат файлов для хранения 3D-объектов. Он назы­ вается Х-форматом. Мы будем использовать именно этот формат в нашем графическом ядре для хранения данных об объектах.

Зам ечание М ы рассмотрим сетчатые модел и , материалы и текстуры только в том объ­ еме, который нам необходим , чтобы приступить к моделированию физики. Если вы хотите глубже изучить текстурирование ( вам оно почти наверняка понадобится, чтобы создавать реалистичные игр ы ) , прочитайте другие книги, например, Mason McCuskey «Special Effects Game Programming with DirectX» (издательство Premier Press) ил и David Fransoп «The Dark Side of Game Texturing» (издательство Premier Press) .

Все, что мы сделаем с Х-файлами, - найдем несколько моделей и за­ грузим их . Можно создать Х-файл собственными силами в программе 3D-моделирования или скачать его с сайта, на котором доступны бесплат­ ные модели. Пример - раздел Free Stuff сайта http://www. 3DCafe.com.

1 39

Сетч атые м одел и и Х - ф а й л ы

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

Тексту р ы Текстуры - это растровые рисунки, которые можно наносить на поверх­ ность треугольника как обои. В умелых руках текстуры могут здорово улуч­ шить вид модели. Если вы посмотрите на большинство персонажей игр, то убедитесь, что большая часть глубины и структуры этих персонажей опре­ деляется не количеством вертексов в их моделях, а использованными тек­ стурами. Именно текстурь1 создают впечатление правдоподобия персонажа. Если вы умеете использовать текстуры, вы сможете добиться многого. Текстуры наносятся на сетчатую модель (mesh) с помощью текстур­ ных координат (texture coordinates) . На рисунке 6. 1 показана текстура с текстурными координатами (u, v). Текстурные координаты начинаются с (О, О) в верхнем левом углу и заканчиваются ( 1 , 1 ) в нижнем правом. ( 1 , О)

(О, 1 ) Рис. 6 . 1 . Текстура

(1 , 1 )

и связанные с ней координаты

:Каждому вертексу в сетчатой модели присваиваются текстурные ко­ ординаты. Если вы хотите, чтобы текстура растягивалась по всей поверх­ ности многоугольника, то нужно совместить углы многоугольника и углы текстуры. Например, текстура на рисунке 6 . 1 растянута по всему прямоугольнику, на который она наложена. Предположим, что углы прямоугольника имеют координаты (-1 . 5 , 1 ), ( 1 . 5, 1), ( 1 . 5 , -1) и (-1 . 5, -1), начиная с верхнего левого и перебирая вертексы по часовой стрелке. Чтобы растянуть текстуру на весь прямоугольник, нужно задать верхнему левому вертексу текстурные координаты (О, 0). При этом верхний левый угол текстуры будет совмещен с верхним левым углом прямоугольника. Верхнему правому вертексу прямоугольника нужно присвоить текстурные координаты ( 1 , О), а нижнему правому - ( 1 , 1 ) . И, наконец, нижнему лево­ му вертексу нужно присвоить текстурные координаты (0, 1).

Глава 6

1 40

Применяя текстуру, DirectX заполняет ею поверхность полигона с по­ мощью интерполяции. Элементы растрового изображения называются пикселями (pixel), а элементы текстуры называются текселями (texel). Интерполяция сводится к назначению текселя каждой ячейке на поверх­ ности многоугольника. (О, О}

(1, 1 ) Рис. 6 . 2 . Текстурированный треугольник

Посмотрите на рисунок 6 . 2 . У изображенного на нем треугольника есть три вертекса. Верхнему вертексу присвоены текстурные координаты (0, О), поэтому на него накладывается компонент (О, О) текстуры с рисун­ ка 6. 1 . У правого вертекса текстурные координаты равны ( 1 , 1 ) , а у лево­ го вертекса (О, 1 ) . После задания текстурных координат текстура накладывается на поверхность с помощью интерполяции. Обратите внимание на то, как текстура искажается, когда DirectX приходится наносить квадратную текстуру на треугольный участок. Большинство текстур имеют квадратную форму. Обычно они наносят­ ся на квадратные участки, чтобы избежать искажения . Это делается не всегда, но обычно это так. -

С оздание текстур из файлов Если у вас есть растровый рисунок , хранящийся в файле, этот рисунок можно применить в качестве текстуры . Сначала нужно создать указатель на текстуру: LPDIRECT3DTEXТURE9 pTexture

=

NULL ;

Непосредственно создать текстуру можно с помощью функции Direct3D с довольно длинным, но легко запоминающимся названием DЗDXCreate­ TextureFromFile ( ) .

Сетч атые модел и и Х -фа й л ы

141

Эта функция может загружать текстуры из файлов нескольких ти­ пов, перечисленных в таблице 6. 1 . Заметьте, что она не поддерживает файлы . gif, . рсх и . tif. Таблица 6 . 1 . Типы файлов , поддержи ваемые функцией

DЗDXCreateTextureFrom File() Рас ш ирение

Тип

. bmp

Растровые рисунки Windows

. dds

Поверхности DirectDraw

. jpg

Файлы изображений JPEG

. png

Файлы PortaЫe N etwork Graphics

. tga

Файлы Targa

Предположим, что мы хотим создать текстуру из файла lava . jpg. Это можно сделать одной строкой: DЗDXCreateTextureFromFile ( pDevice , " lava . jpg " , pTexture ) ;

Если вы хотите более подробно настроить процесс применения тексту­ ры, можно воспользоваться функцией DirectX DЗDXCreateTextureFrom­ FileEx ( ) . У этой функции 14 параметров! В общем, я рекомендую обходиться функцией DЗDXCreateTextureFromFile ( ) , если только у вас нет убедительных причин не делать этого.

З адание текстур Любой 3D-игре приходится работать одновременно с множеством текстур, поэтому нужно указывать, какая текстура активна, прежде чем начинать рендеринг. Это можно сделать с помощью метода IDirect3DDevi ­ ce 9 : : SetTexture ( ) . Первый параметр этой функции - индекс для текстур, позволяющий выбрать для наложения до восьми текстур. Второй параметр этой функции указатель на созданную ранее структуру. Возможность выбора нескольких текстур используется для мультитекстурирования (multitexturing), до­ вольно сложного графического приема, с которым мы возиться не будем. Так что первому параметру мы присвоим значение О. Вот, собственно говоря, и все. Задать текстуру несложно : pDevice- >SetTexture ( О , pTexture ) ;

Гл ава 6

1 42

М а териал ы В Direct3D материал определяет , как объект выглядит при освещении. У материала есть пять свойств, объединенных в структуру DЗDМATERIAL9 : typedef struct _DЗDМATERIAL9 DЗDCOLORVALUE Diffuse ; DЗDCOLORVALUE AmЬient ; DЗDCOLORVALUE Specular ; DЗDCOLORVALUE Emissive ; float Power ; D ЗDМATERIAL9 ; О

Элемент Diffuse определяет цвет объекта в падающем свете. Количество отражаемого света определяется углом падения света на поверхность.

о

Элемент Amblent определяет цвет объекта в рассеянном свете, то есть свете, не приходящем из отдельного четко определенно­ го источника. Эти элементы определяют базовый цвет объекта. Обычно, что­ бы соответствовать реальному миру, эти цвета должны совпа­ дать.

о

Элемент Specular определяет цвет блестящих частей объекта. Обычно этому элементу присваивается белый цвет . Увеличе­ ние значения элемента Power увеличивает резкость блестя­ щих частей.

о

Элемент Emissive определяет цвет свечения объекта. Учтите, что объект, сделанный светящимся таким образом , не будет освещать окружающие объекты.

Все, что нужно, чтобы задать цвет - объявить экземпляр структуры D3DМATERIAL 9 , заполнить его данными и передать методу IDirect3DDe­ vice9 : : Setмaterial ( ) , например: DЗDМATERIAL9 Мaterial ; 1 1 Задайте цвета в структуре Мaterial pDevice->Setмaterial ( &Мaterial ) ;

З а груз ка се тча то й модел и Загрузить сетчатую модель довольно просто - это делает функция DЗDXLoadМeshFromX ( ) . Эта функция создает буферы для хранения мате­ риалов и текстур, которые хранятся в Х-файле. Прежде чем можно будет

Сетч атые модел и и Х - фа й л ы

1 43

загрузить модель, нужно объявить указатели na буфер материалов и ин­ терфейс модели и создать переменную типа DWORD для хранения количе­ ства материалов: LPDЗDXМESH theМesh ; LPDЗDXВUFFER materialsBuffer DWORD numМaterials 01;

=

NULL ;

=

Теперь можно загрузить модель в память: DЗDXLoadМeshFromx ( meshFileName , DЗDXМESH_МANAGED , dЗdDevice , NULL , & material sBuffer , NULL , & numМaterial s , & theМesh) ;

Здесь me shFileName

-

имя Х-файла, в котором хранится модель.

И звлечен ие текстур и материал ов После того, как файл загружен, нужно извлеч�, текстуры и материалы из буфера материалов . Это необходимо , потому что буфер материалов содер­ жит и структуру D3DМATERIAL9 с информацие:ti: о свойствах материала, и имя файла, содержащего текстуру. Чтобы извлечь текстуры и материалы из буфера материалов, нужно создать массивы для текстур и материалов: DЗDМATERIAL9 *pМaterials ; LPDIRECTЗDTEXTURE 9 *pTextures ; // Создаем новые массивы текстур и материаJJ ов . pTextures new LPDIRECT3DTEXTURE 9 [numTextures] ; pМaterials = new DЗDМATERIAL9 [ numМaterials) ; =

Кроме того, нам понадобится указатель на Irачало буфера материалов. Этот указатель можно получить с помощью :метода IDirect3D 9 : : Get­ BufferPointer ( ) : // Получаем указатель на буфер материалов . DЗDXМATERIAL* pМatвufferPointer (DЗDXМATERIAL* ) pМaterialBuffer->GetвufferPointer ( ) ; =

Теперь можно по очереди перебрать все м�териалы в буфере. Общее количество материалов указано в переменной numМaterial s . Просмат­ ривая содержимое буфера, программа заполннет массив pмaterials и

Глава 6

1 44

загружает текстуры в массив pTextures . Если в буфере материалов, за­ груженном из Х-файла, не указаны текстуры , то указатель на текстуру устанавливается в NULL: 1 // Перебираем материалы . 2 for (DWORD i = О ; iRelease ( ) ;

Сетч атые модел и и Х -фа йл ы

1 45

Р ендеринг сетчатой модел и Сетчатая модель делится на части, каждая из которых х арактеризуется материалом и текстурой. Программа должна перебирать эти части и вы­ полнять рендеринг каждой из них по отдельности : // Перебираем части модели по материалам . О ; iSetмaterial ( &pмaterials [ i ] ) ; dЗdDevice->SetTexture ( O , pTextures [ i ] ) ; // Рисуем часть модели . pМesh- >DrawSuЬset ( i ) ;

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

Очистка сетчатой модели Закончив работу с моделью, нужно удалить ее из оперативной памяти. Сначала удаляем массив материалов : delete [ ] pMaterial s ;

Затем освобождаем интерфейсы для всех структур : for ( DWORD i

=

О ; iRelease ( ) ;

Освободив эти интерфейсы, можно удалить массив текстур , посколь­ ку он нам больше не нужен: delete [ ] pTextures ;

И, наконец , освобождаем интерфейс модел:fl : pМesh- >Release ( ) ;

1 46

Глава 6

Кла сс d3d mesh Вся описанная выше функциональность есть в классе dЗd mesh. Этот класс может загружать, отображать и очищать сетчатые модели. Кроме того, класс dЗd_mesh поддерживает возможность, необходимую для большинства игр. Часто в играх отображается множество экземпляров одинаковых объектов. Поиграйте в трехмерную « бродилку� . На стенах ла­ биринта есть факелы? В памяти на самом деле находится только одна мо­ дель факела, которую игра использует для отображения всех факелов. В классе dЗd_me sh есть специальные элементы, позволяющие ис­ пользовать в с цене одну и ту же модель много раз . Мы рассмотрим эти элементы подробно в разделе « Подсчет ссылок в классе d3d_mesh>) да­ лее в этой главе. Определение класса dЗd_mesh приведено в листинге 6. 1 . Л истинг 6 . 1 . Класс dЗd_mesh 1 2 3 4 5 б

7 8 9

10 11 12 13 14 15 16 17 18

class d3d mesh private : class mesh data puЫic : LPD3DXМESH theMesh ; D3DМATERIAL9 *al lМaterials ; LPDIRECT3DTEXTURE 9 *allTextures ; int totalMaterials ; int referenceCoun t ; //РuЫiс-ме'l'оды . mesh_data ( ) ; -mesh_data ( ) ; };

19 mesh data *meshData ; 20 21 2 2 puЫic : 23 d3d_mesh ( ) ; d3d_mesh ( 24 25 d3d_mesh &sourceMesh) ; 26 -d3d_mesh ( ) ; 27 28 d3d_mesh &opera tor = ( d3d_mesh &sourceМesh ) ; 29 30

Сетч атые модел и и Х-ф а йл ы 31 32 33 34 } ;

1 47

Ьооl Load ( std : : string fileName ) ; Ьооl Render ( ) ;

Класс dЗd_mesh содержит всю информацию, необходимую для за­ грузки и рендеринга сетчатой модели . Заметьте, что он содержит другой класс. Такой прием часто применяется в профессиональном программи­ ровании на С++, но я обычно избегаю использовать его в примерах, по­ скольку листинги, в которых он используется, трудно разбирать. Здесь он используется, чтобы можно было реализовать прием, известный как подсчет ссылок: (reference counting). Объекты с подсчетом ссылок хранят данные точно так же, как и обычные объекты, однако они позволяют другим объектам обращаться к хранимым данным через указатель. Объ­ екты с подсчетом ссылок подсчитывают количество объектов, указываю­ щих на хранимые данные. При этом данные не удаляются, пока есть объекты, указывающие на эти данные. Именно эту функцию и выполня­ ет класс mesh data. И в классе dЗd_mesh, и в классе mesh_data есть методы, необходи­ мые им для работы. Все методы класса dЗd_mesh обращаются к данным через динамически созданный объект класса mesh data. Посмотрим, как работает класс dЗd_mesh.

З а грузка сетчатой модел и в классе dЗd_mesh Чтобы загрузить модель в объект класса dЗd_me sh, нужно вызвать его метод Load ( ) , код которого приведен в листинге 6 . 2 . Листинг 6 . 2 . Метод dЗd_mesh: : Load ( ) 1 Ьооl d3d_mesh : : Load ( std : : string fileName ) 2 { Ьооl meshLoaded=fal se ; 3 LPD3DXВUFFER tempМaterialbuffer=NULL ; 4 D3DXМATERIAL *materialBuffer=NULL ; 5 б НRESULT hr = D3DXLoadМeshFromX ( 7 fileName . c_s tr ( ) , 8 D3DXМESH_SYSTEММEМ, 9 theApp . D3DRenderingDevice ( ) , 10 11 NULL , 12 &tempМaterialbuffer , 13 NULL , (DWORD * ) &meshData->totalМaterial s , 14 &meshData->theМesh) ; 15 16 / / Если модель загружена . . . 17

1 48 18

Глава 6 if ( hr=D3D_ОК)

19 // Создаем буфер материалов materialBuffer = (D3DXМAТERIAL * ) tempМaterialЬuffer-> GetвufferPointer ( ) ; meshLoaded=true ;

20 21 22 23 24 25 26

27 28 29 30

if (meshLoaded==true ) // Выделяем массив для материалов . meshData-allМaterials = new D3DМATERIAL9 [meshData->totalМaterial s ] ;

31 32 33

34 35 36

{

/ / Если .массив вы.целить не у,;�алось . . . if (meshData->allМaterials=NULL ) meshLoaded=false ;

37 38

39 40

41 42 43 44 45 46 47 48 49 50 51 52 53

54 55 56 57 58 59 60 61 62

63 64 65 66

i f (meshLoaded=true ) // Выделяем массив для текстур . meshData->allTextures = new LPDIRECT3DTEXTURE 9 [meshData->totalМaterials ] ; / / Если массив вы.целить не удалось . . . if (meshData->allTextures==NULL) // Освобождаем массив материалов . delete [ ] meshData->allМaterial s ; meshLoaded=false ;

i f (meshLoaded=true ) for ( int i=O ; ( itota1Мaterials) ; i++ // Копируем материалы . meshData->allМaterials [ i ] =materialBuffer [ i ] . МatD3D ; / * Задаем цвет материала в рассеянном цвете таким же , что и в свете точечного источника . Это о бычно делается для реалистичного рендеринга . * / meshData->allМaterials [ i ] . AmЬient =

Сетч атые модел и и Х - фа й л ы 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98

1 49

meshData->allМaterials [ i ] . Diffuse ; // Если в Х-файле указано имя файла текст;уры . . . if ( (materialBuffer [ i ] . pTextureFilename ! = NULL ) & & ( lstrlen (materialBuffer [ i ] . pTextureFilename) > О) )

{

// ЗагрУЖаем текст;уру . i f ( FAILED (DЗDXCreateTextureFromFile ( theApp . D3DRenderingDevice ( ) , materialBuffer [ i ] . pTextureFilename , &meshData->allTextures [ i ] ) ) ) / * Е сли текст;уру не удалось загрузить , задаем возвращаемое значение , сообщающее об ошибке . */ meshLoaded=false ;

}

// Есnи текст;уры нет . . . else meshData->allTextures [ i ] = NULL ;

// Разобрались с буфером материалов . Освобождаем его . . . tempMaterialbuffer->Release ( ) ;

return (meshLoaded) ;

Первое, на что стоит обратить внимание в методе Load ( ) , - данные сохраняются в объекте класса mesh_da ta. Как я уже говорил, это делает­ ся для подсчета ссылок, который мы рассмотрим немного позже. У метода Load ( ) есть единственный параметр - имя Х-файла, из ко­ торого загружается модель. Объявив некоторые нужные ему перемен­ ные, метод Load ( ) вызывает функцию DЗDXLoadМeshFromX ( ) , чтобы загрузить Х-файл (строки 7 - 1 5 листинга 6.2). Если файл успешно загру" жен, метод получает указатель на буфер материалов в строках 2 1 - 23. Далее метод Load () выделяет массив структур материалов в строках 30-31. Если выделение массива проходит успешно, метод перебирает все материалы в списке в строках 58- 9 1 . В теле цикла метод копирует данные о материалах из буфера в массив материалов в текущем объекте. При этом цвет в рассеянном свете задается равным цвету в свете точечного источни­ ка (строки 66-67). Если с текущим материалом связана текстура, метод

Глава 6

1 50

Load ( ) пытается загрузить ее. Если это не удается, то что-то не так, и воз­ вращается значение, свидетельствующее об ошибке. Затем это значение передается вызывающей функции. Если с материалом не связаны никакие текстуры, метод Load ( ) просто присваивает указателю значение NULL.

П р едупр еждение М етод dЗd mesh : : Load () м ожет загрузить не любую сетчатую модель Di­ rectЗD, кото рая может храниться в Х-файле. Он загружает только простые сетчатые модели, которые мы будем использовать в оставшейся части книги. За дополнительной информацией об Х-файлах и загрузке моделей обратитесь к литературе. Я рекомендую книгу Wolfgaпg F. Engle «Beginning DirectЗD Game Programming" ( издательство Premier Press) . Это одна из не­ многих книг, в которой подробно разбираются Х-файлы и их применение.

Р ендеринг сетчатой модели в классе dЗd_mesh Рендеринг модели значительно проще, чем ее загрузка. Как уже говори­ лось ранее, модели состоят из частей. Поэтому, чтобы выполнить ренде­ ринг модели, программа должна перебрать все части модели и для каждой части выполнить следующие шаги: 1.

2. 3.

Выбрать для части материал. Выбрать для части текстуру, если она задана. Выполнить рендеринг части.

Метод dЗd_mesh : : Render ( ) , код которого приведен в листинге 6 . 3 , выполняет эти шаги. Л истинг 6 . 3 . Метод dЗd_mesh : : Render( ) 1 2

З 4 5 б

7 8 9 10 11 12 13 14 15

16

Ьооl dЗd_mesh : : Render ( ) { Ьооl meshRendered=true ; /* Модели депЯ'l'ся на части - по одной для каждого иа'l'ериала . Рендеринг каждой час'l'и нужно выполня'l'ь О'l'дельно . * / for (DWORD i=O ; i< (DWORD ) meshData->totalМaterials ; i++ { // Задаем ма'l'ериал и 'l'eкc'l'ypy для час'l'и i f ( theApp . DЗDRenderingDevice ( ) ->Setмaterial ( &meshData->allMaterials [ i ] ) ! = DЗD_OK) meshRendered=false ; }

if ( theApp . DЗDRenderingDevice ( ) ->SetTexture (

151

Сетч атые модел и и Х - ф а й л ы 17 18 19 20 21 22 23 24 25 26 27

O , meshData->allTextures [ i ] )

!= DЗD_OK)

meshRendered=false ;

// Выполняем рисование части . meshData->theМesh->DrawSuЬset ( i ) ;

} return

(meshRendered) ;

Метод Render ( ) и з листинга 6 . 3 выполняет три шага для рендеринга каждой части модели с помощью Direct3D. С помощью цикла, начинающе­ гося в строке 7, перебираются все части модели. При каждом проходе цикла метод Render ( ) вызывает функцию Direct3D LPDIRECTЗDDEVICE9 : : Set­ мa terial ( ) , чтобы задать материал части, рендеринг которой нужно вы­ полнить. Далее, чтобы задать текстуру части, метод Render ( ) вызывает функ­ цию Direct3D LPDIRECTЗDDEVICE9 : : SetTexture ( ) , а чтобы выполнить собственно рендеринг этой части - функцию DrawSuЬset ( ) .

О п тим и з а ц и я

в

м етоде

Render()

Я целенаправленно проигнорировал возможность опти мизировать м етод dЗd_mesh : : Render ( ) . Оператор if в строках 1 6-20 стоит поместить в еще один оператор if, проверяющий, равен ли NULL элемент массива allTex­ tures [ i ] . Если да, то не нужно вызывать функцию SetTexture ( ) , посколь­ ку для части не задана текстура. Это ускорит выполнение рендеринга.

Так почему же этого оператора if нет? Я пропустил его, поскольку заранее знал, что все модели, которые я буду испол ьзовать в книге, содержат текстуры для всех своих частей. Если у ка­ кой-то части нет текстуры, что-то не так. Кроме того, я по возможности пропускаю п роверку ошибок и оптимизации в п ри м ерах из книги, если это возможно, чтобы код оставался максимально простым и понятн ы м . Я упоминаю о б этом , поскольку вы можете попытаться применить этот код в реальной игре. Если вы это сделаете, учтите, что некоторым частям мо­ делей могут быть не назначены н и какие текстуры . Если такая возможность существует, добавьте в код этот оператор if. Это позволит повысить п ро­ изводительность.

Подсчет сс ылок в классе dЗd mesh Как уже упоминалось, в игре одна и та же модель может использоваться для создания нескольких экземпляров объектов. Чтобы позволить такое использование класса dЗd_mesh, в нем реализован подсчет ссылок.

Глава 6

1 52

Чтобы не хранить данные модели в классе dЗd_me sh, данные хранят во вспомогательном классе mesh data. В классе dЗd mesh есть динами­ чески выделяемый объект класс-; mesh_data. На од;н и тот же объект mesh data может ссылаться несколько объектов dЗd me sh, и объект mesh:=data отслеживает все объекты dЗd_mesh, указы-;ающие на него, как показано на рисунке 6 . 3 .

dЗd mesh

dЗd_mesh

mesh data LPDЗDMESH theMesh, DЗDMATERIAL "alMateпals, LPDIRECTЗDTEXТURES "afTextures, lпt totalMater1als, lnt refereпceCouпt,

d Зd_mesh

Рис . 6 . 3 . Множество объектов dЗd_mesh, указывающих на один объект

mesh_data

На рисунке 6.3 показаны три объекта dЗd_mesh, в которых есть ука­ затели, изображенные в виде стрелок. Все три объекта обращаются к од­ ним и тем же данным, и их указатели указывают на один и тот же объект mesh_data. В объекте mesh_data есть элемент referenceCount, в кото­ ром хранится количество объектов dЗd_mesh, ссьшающихся на этот объект.

Сетч атые модел и и Х -фа йл ы

1 53

Чтобы понять, как выполняется подсчет ссь1лок, для начала еще раз посмотрим на определение класса dЗd_mesh. Для удобства оно повторено ниже в листинге 6.4. Листинг 6 . 4 . Класс dЗd_mesh 1

class d3d_mesh

2

{

3

private :

4

class mesh data

5 6

puЬlic :

7

LPD3DXМESH theМesh ;

8

D3DМATERIAL9 *allМaterials ;

9

LPDIRECT3DTEXTURE9 *allTextures ;

10

int totalМaterials ;

11 i n t referenceCount ;

12 13 14

/ /РuЬliс-ме'l'оды .

15

mesh_data ( ) ;

16

-mesh_data ( ) ;

17 18

};

19 20

mesh data *meshData ;

21 22 puЬlic : 23 d3d_mesh ( ) ; 24 d3d_mesh ( 25 26

d3d mesh & sourceМesh) ; -d3d_mesh ( ) ;

27 28 29

d3d_mesh &operator =

(

d3d_mesh &sourceМesh) ;

30 31 32 33

Ьооl Load ( s td : : s tring fileName ) ; Ьооl Render ( ) ;

34 } ;

Определение класса me sh data находится в private-paздeлe опреде­ ления класса dЗd me sh. Поэтому объекты класса me sh data могут при­ сутствовать только в объектах класса dЗd i:nesh . Э:ii"ементы данных класса mesh data это риЫiс-э.лементы, ч;-о довольно необычно для Ьlк е С++. Элементы данных редко объявляются как puЬlic, класса в яз -

Глава 6

1 54

но этот случай - исключение из правила. Поскольку определение класса mesh data содержится в private-paздeлe определения класса dЗd mesh, и по�ольку объектам класса dЗd_mesh нужен быстрый доступ к дан­ ным в объектах класса mesh_data, элементы данных класса me sh_data объявлены как puЬlic.

П редупреждение Опытные программисты, хорошо знающие я з ы к С++, заметят, что можно и не объявлять элементы данных класса mesh_data как puЫic. Их можно объявить как private и создать встраиваемые (iпliпe) рuЫiс-методы доступа к ним. Использование этих методов позволит избежать снижения скорости при доступе к данным. Это правда. Компилятор С++ подставляет код встраиваемых методов в места их вызова, примерно так же , как препро­ цессор - макрокоманды в программах на С. Если вы реализуете класс с подсчетом ссылок более сложный, чем класс mesh_data, я настоятельно рекомендую создать рuЫiс-методы доступа к его элементам данных, а сами элементы данных объявить как private. Хотя код станет несколько более объем ным, он будет надежнее, а использова­ ние встраиваемых методов не приведет к снижению скорости.

В классе mesh_data есть свои методы. Точнее говоря, в нем есть кон­ структор и деструктор. Их код приведен в листинге 6 . 5 . Листинг 6 . 5 . Методы класса mesh_data 1 2

inline dЗd_mesh : : mesh_data : : mesh_data ( )

{

3

theМesh=NULL ;

4

allMaterials=NULL ;

5

allTextures=NULL ;

6

totalмaterials=O ;

7

referenceCount=l ;

8 9 10 1 1 inline dЗd_mesh : : mesh_data : : -mesh_data ( ) 12 13

if

(allмaterial s ! =NULL)

14 delete

15

[]

allмaterial s ;

16 17 18

if

( allTextures ! =NULL)

19 20 21

for ( int i=O ; iRelease ( ) ;

25 26 delete

27

[]

allTextures ;

28 29 30

if

( theMesh ! =NULL)

31 32

theMesh->Release ( ) ;

33

theMesh=NULL ;

34 35

Каждый раз, создавая объект класса dЗd_mesh (и, соответственно, объ­ ект класса mesh_data), игра должна инициализировать данные в объекте mesh_data значениями О или NULL. Эту инициализацию выполняет кон­ структор клacca mesh_data. Кроме того, конструктор присваивает элемен­ ту referenceCount значение 1 , поскольку, если объект клacca mesh_data создается, то к нему обращается как минимум один объект dЗd_mesh. Единственная функция, которая должна будет записывать данные в модель в этой программе, - это метод dЗd_mesh : : Load ( ) . В этой реали­ зации данные в модели не изменяются в ходе работы программы. Это утверждение может не соответствовать истине в некоторых играх. На­ пример, можно загрузить плоскую модель участка местности с данными о текстурах и материалах , а затем создать на этом участке холмы и впа­ дины, изменяя координаты у отдельных вертексов. В этом случае про­ грамма должна предоставлять доступ к отдельным вертексам . В нашем примере такая возможность не нужна, и мы ее не предоставляем. При удалении объекта класса mesh_data вызывается деструктор, приведенный в строках 1 1 -35 листинга 6.5. Деструктор выполняет опера­ ции, рассмотренные ранее в разделе « Очистка сетчатой модели » . Сначала он удаляет массив материалов. Вспомните, что это массив структур, ко­ торые можно просто освободить. С другой стороны, текстуры хранятся в массиве указателей на СОМ-объекты. Поэтому деструктору приходится просматривать весь массив и вызывать функцию СОМ Release ( ) для каждого элемента этого массива. Это делает цикл, приведенный в стро­ ках 20-26 листинга 6 . 5 . После освобождения всех структур деструктор удаляет их массив в строке 27. И, наконец, деструктор освобождает саму сетчатую модель. Подсчет ссылок выполняют основной конструктор, конструктор ко­ пирования, деструктор и оператор присваивания в классе dЗd mesh. Их код приведен в листинге 6.6. -

1 56

Глава 6

Листинг 6 . 6 . Методы подсчета ссылок 1

inline d3d_mesh : : d3 d_mesh ( )

2

{

3 4 5 6 7

/ * Сейчас функция не проверяет выделение ресурсов . Это делается позднее . * /

meshData = new mesh_data ( ) ;

8 inline d3d mesh : : d3d mesh ( 9 d3d mesh &sourceмesh) 10 11 meshData = sourceMesh . meshData ; 12 sourceМesh . meshData->referenceCount++ ; 13 14 1 5 inline d3d_mesh &d3d_mesh : : operator = dЗd_mesh &sourceMesh) 16 17 18 // Если они н е одно и т о же . . . 19 if (meshData • =sourceМesh . meshData) 20 21 // Данные уходят . Уменьшим значение счетчика . meshData->referenceCount - - ; 22 23 24 // Если это последний объект , обращающийся к данным . . . 25 if (meshDat&->referencecount== O ) 26 { delete meshData ; 27 _

_

28 29 30 / / Привяз:ыв&ем объект к данным . 31 meshData = sourceМesh . meshData ; 32 / / Увеличиваем значение счетчика ссьшок . 33 34 sourceМesh . meshData->referenceCount++ ; 35 36 37 return ( * this ) ; 38 39 4 О inline d3d_mesh : : -dЗd_mesh ( ) 41 42 11 :Уменьшаем зна чение сче!Z'чика при ;уничr.rожении объекr.rа . meshData->referenceCount- - ; 43 44 / / Если это ПОС.!Iедний объект , обращавшийся к данным . . . 45 i f (meshData->referenceCount= O ) 46 47 { delete meshData ; 48 49 50

Сетч атые модел и и Х - фа й л ы

1 57

Создавая объект класса dЗd_mesh, программа вызывает конструктор этого класса. Этот конструктор приведен в строках 1-6 листинга 6 . 6 . Все, что делает конструктор - создает объект класса mesh_data и сохраняет его адрес в элементе данных meshDa ta. Как уже говорилось ранее, конст­ руктор класса mesh data инициализирует все элементы данных и при­ сваивает элементу r-; ferenceCount значение 1 . Затем программа может загружать и отображать модель.

П редуп реждение Чтобы сконцентрироваться на подсчете ссылок и рендеринге моделей, я не добавил в конструктор класса dЗd mesh никаких механизмов обнару­ жения и обработки ошибок. Никогда не поступайте так в реальных играх это почти наверняка приведет к неработоспособности этих игр. В последу­ ющих главах мы добавим обработку ош ибок в конструктор.

Конструктор копирования и оператор присваивания позволят исполь­ зовать одну и ту же модель нескольким объектам одновременно. Конст­ руктор копирования прост; он создает новый объект, и нет вероятности того, что создаваемый объект уже связан с какой-то моделью. Поэтому конструктор копирования просто помещает в указатель meshData в со­ здаваемом объекте адрес из такого же указателя в исходном объекте. При этом нужно увеличить значение в элементе referenceCount , пос:кольку к данным начинает обращаться еще один объект. Предположим, что программа объявляет три объекта класса dЗd_mesh, названные meshl , mesh2 и meshЗ. Теперь представим, что в объект meshl загружена модель. После этого выполняется такой оператор: meshЗ = mesh2 = meshl ;

Для выполнения этого оператора вызывается оператор присваивания класса dЗd mesh. Код этого оператора приведен в строках 15-38 листин­ га 6 . 6 . 3ад;-ча, стоящая перед оператором, сложнее, чем стоящая перед конструктором копирования. Во-первых, может оказаться, что объект-ис­ точник и объект-получатель - это один и тот же объект. Проще говоря, программа может содержать оператор вроде meshl = meshl ;

Да, та:ких операторов быть не должно, но, тем не менее, они встреча­ ются. Оператор if в строке 19 листинга 6 . 6 предотвращает выполнение оператором присваивания всех действий, если объект-источник и объ­ ект-получатель уже обращаются к одному и тому же объекту класса mesh data. Если объект-источник и объект-получатель обращаются к разным объектам класса mesh_da ta, то оператор присваивания уменьшает значе­ ние счетчика ссылок в объекте-получателе. Это необходимо сделать, по­ скольку после присваивания объект-получатель будет обращаться к другим данным - к тем же , к :которым обращается объект-источник.

1 58

Глава 6

После уменьшения значения счетчика может оказаться, что к объек­ ту класса mesh_da ta больше не обращается ни один объект класса dЗd_mesh. Если это так, то оператор присваивания удаляет объект класса mesh_data в строках 25-28. Затем оператор присваивания записывает в указатель meshData адрес объекта класса mesh data, на который указывает указатель meshData в копируемом объекте :-Это делается в строке 3 1 . Кроме того , он увеличивает значение счетчика ссыло:к, поскольку к объекту класса mesh_da ta теперь обращается еще один объект класса dЗd_mesh. Оператор присваивания за­ вершается как обычно - возвращая копию объекта-получателя. Единственная оставшаяся задача в подсчете ссылок - удаление объек­ тов класса dЗd mesh. Деструктор этого класса приведен в строках 40-50 . Когда удаляете-;;: объект класса dЗd mesh, он перестает обращаться к дан­ ным модели. Поэтому деструктор уменьшает значение счетчика обраще­ ний в строке 43. Если к объекту класса mesh_da ta больше нет обращений, этот объект тоже можно удалить. Деструктор класса mesh_data выполня­ ет операции очистки, требуемые Direct3D.

П одска зка Методика подсчета ссылок основана на подходе, описанном в статье 29 книги Scott Meyers «Маге Effect1ve С++» (издательство Add1son- Wes/ey) . Эта книга и связанная с ней книга «Effect1ve С++» (те же автор и издатель­ ство) очень полезны всем програм мистам , работающим на языке С++. Прочитав их вы сможете заметно повысить уровень своих познаний в С++.

И то г и На компакт-диске, поставляемом с книгой, есть пример программы, по­ казывающий, как использовать класс dЗd_mesh. Он находится в папке Source\ChapterO б \MeshSpin. В программе используется сетчатая мо­ дель тигра, поставляемая в составе SDK DirectX . Готовя программу к компиляции, скопируйте файлы tiger . х и tiger . Ьmр из подпапки SDK Media в папку проекта. При работе программа должна отображать вращающуюся модель тигра на синем фоне. Мы продвинулись от рендеринга простых объектов, состоящих из нескольких треугольников, до рендеринга сложных объектов, представ­ ленных сетчатыми моделями с текстурами. Научившись выполнять рендеринг сложных объектов, мы можем перейти к моделированию движения ЗD-объектов.

Часть 1 1

З D - объ ект ы , д в и ж е н и е и стол к нов е н и я Глава 7 Динамика материальных точек Глава 8

Столкновения материальных точек Глава 9 Динамика твердых тел Глава 10 Столкновения твердых тел Глава 11 Сила тяжести и метательные снаряды Глава 12 Системы масс и пружин Глава 13 Вода и волны

Гл а ва 7

Ди н а м и ка м а те риал ьн ы х то ч е к Эта глава посвящена одной из основных тем классической физики: дви­ жению материальных точек. Это обширная тема, и ее знание позволяет реализовать некоторые весьма впечатдяющие эффекты, но это знание даст нам еще и возможность перейти к работе с твердыми и деформируе­ мыми телами. Кроме того, в этой главе вы познакомитесь с несколькими силами, моделирование которых нам по:tlадобится и даст возможность силь­ но увеличить реалистичность игр. Кроме того, в этой главе изложены нача­ ла математического анализа. Вперед!

М а териал ьные точ ки Объекты, с которыми мы будем работать в этой главе, обладают одним об­ щим свойством: они намного меньше, чем расстояния, на которые они перемещаются . Представьте себе маш11ну, едущую по шоссе. Если рас­ сматривать ее вблизи, как на рисунке 7 . 1 , будет видно, что у нее есть определенная форма, есть размеры, лю,о;и могут двигаться в салоне, двери могут открываться и так далее. Во вреl\'IЯ движения по дороге колеса ма­ шины вращаются. На машину действует множество сил - сила сопротив­ ления воздуха, давление ветра, дующеrо на машину с одной стороны или с другой, трение между шинами и поверхностью дороги и так далее.

Рис. 7 . 1 . Маш ина, едущая по шоссе, выглядит сложной, если рассматривать ее вблизи

1 61

Дин а м и ка м атери ал ьных точек

Все это можно моделировать - но это сложно. Физики и программи­ сты, занимающиеся физическим моделированием, обычно начинают с упрощения ситуации. Первый шаг - немного отодвинуться. На рисун­ ке 7 . 2 мы видим то же движение, но в более мелком масштабе. Машина выглядит почти точкой . Все маленькие элементы уже незаметны. Соот­ ветственно , положение машины, описанное вектором в некоторой систе­ ме координат, - это достаточно хорошее представление ситуации. 1 1 \ \ \ \ \ '

1

1 1

1

1

1

1

1 1

1

1 1 1 1 1 1 1 1

1 1 1 1 1

Рис. 7 . 2 . Та же машина, рассматриваемая издалека, может быть описана единственным вектором, определяющим ее положение в заданный момент вре мени

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

Глава 7

1 62 П одска зк а

Ключ к хорошему физическому м оделированию (и быстро выполняющемуся коду) - умение определять, что нужно модел ировать и вычислять, а что - нет.

О дномерна я ки нема т и ка Кинематика - это изучение движения при отсутствии сил. Начнем изуче­ ние кинематики с рассмотрения материальных точек в одномерной сис­ теме координат. Эти точки могут двигаться только по одной прямой. Возможно, частицы, двигающиеся по прямой при отсутствии сил , кажут­ ся не слишком интересным предметом для изучения, но они - хорошее начало для изучения физики, а детали, которые мы пока игнорируем, могут оказаться весьма замысловатыми. Посмотрим.

Скорость Изучение кинематики обычно начинается с понятия скорости. Скорость (velocity) - это расстояние, пройденное за единицу времени. Формулу для нахождения скорости можно записать так: Лх

v= ­

Лt

Здесь v - средняя скорость, Лх - пройденное расстояние, а Лt - время, затраченное на преодоление этого расстояния.

За м е ч ание Скорость измеряется в м илях в час (miles per hour - mph) в США и в метрах в секунду ( м/с) или километрах в час ( км/ч) почти во всех других странах. Один метр в секунду - это приблизительно две м или в час.

Представьте себе, что наша машина проехала мимо знака « Добро пожаловать в Канзас » и движется по длинному прямому участку доро­ ги с приличной скоростью (см. рис. 7 . 3). Если вы никогда не были в Канзасе, поясню - это равнинный штат, и на его примере мы рассмот­ рим правдоподобный случай . П олицейский, сидящий в автомобиле за кустом в 500 метрах от границы штата, запускает секундомер , когда машина пересекает границу штата, и останавливает его , когда машина проезжает мимо куста. Полицейский видит, что прошло всего 10 се­ кунд . Соответственно, можно найти среднюю скорость, с которой ма­ шина преодолела эти 500 метров.

1 63

Дин а мика м атери ал ьных точек

Рис. 7 . 3 . Машина, мчащаяся по дороге в Канзас

v = Лх

/

Лt = 500

м / 10

с =

50 м/ с

50 м/с - это больше 100 миль в час, куда больше, чем ограничение скорости в штате Канзас. Полицейский чувствует себя вправе остановить машину.

Скорость как п роизвод ная Полицейский поправляет солнечные очки и подходит к остановившейс я машине. Водитель машины вздыхает и опускает окно, начиная ритуал. Полицейский заявляет: « Сэр, знаете ли вы, с какой скоростью вы еха­ ли? » Водитель качает головой, и полицейский продолжает: « Больше 1 00 миль в час » . Водитель возражает: « Но я ехал всего 20 минут! Как я мог проехать 1 00 миль в час, если я ехал меньше часа?» Что может ответить на это полицейский? Ну, в общем-то, полицей­ ский не обязан это делать, но знающий физику полицейский мог бы отве­ тить: « Это значит, что если бы вы ехали с этой скоростью целый час, вы бы проехали 1 00 миль » . Водитель отпирается: « Но я тормозил и снижал скорость, и если бы я продолжал ехать, я бы проехал меньше 100 миль!»

Замечание Эта небольшая история о водителе и полицейском взята и з фейнманов­ ского курса л екций по физике ( Фейн ман Р. , Лейтон Р . , Сэндс М. «Фейнма ­ новские лекции по физике» ) . Его автор, Ричард Фейнман - один из величайших физиков и, возможно, величайший из преподавателей физи­ ки . Его л екции - возможно, луч ший начальный учебный курс по физике .

Глава 7

1 64

Проблема полицейского в том, что формула для нахождения скорости позволяет найти только среднюю скорость, с которой объект прошел не­ которое расстояние. Средняя скорость - это обычно не то, что мы подра­ зумеваем, говоря о скорости. Обычно мы имеем в виду мгновенную скорость - скорость в какой-то момент времени . Взгляните на рисунок 7.4 - график расстояния, пройденного тормозя­ щей машиной. Кривая линия обозначает расстояние, пройденное ей за ин­ тервал времени от начала измерения до выбранного момента. Поскольку вертикальная ось соответствует пройденному расстоянию, а горизонталь­ ная ось - прошедшему времени, наклон кривой в любой точке есть ско­ рость. Да, скорость - вектор. У нее есть величина и направление. В данном случае нас интересует величина этого вектора - положительный скаляр.

t

машина остановилась

машина начала тормозить вр е м я

Рис. 7.4. Расстояние , пройденное тормозящей машиной

Обратите внимание, что, когда машина начинает тормозить, расстоя­ ние, которое она преодолевает за единицу времени, начинает уменьшать­ ся. Когда машина остановилась, не важно, сколько еще пройдет времени пройденное расстояние увеличиваться не будет. Попробуем определить скорость машины в какой-то момент времени. Первое приближение такой скорости можно получить, выбрав Лt, распо­ лагающееся в районе точки, для которой мы хотим узнать скорость. Взгляните на рисунок 7. 5 . С помощью графика мы можем найти расстоя­ ние, пройденное в начале и в конце интервала Лt. Разница между этими расстояниями и есть Лх. Средняя скорость в интервале времени Лt как раз и будет равна Лх / Лt. Как видите, между средней скоростью и скоростью, измеренной в ка­ кой-то момент, есть весьма существенное различие - сравните наклон ис­ ходной линии и наклон линии, которую мы использовали, чтобы получить приближенную оценку. Наклон линии равен изменению вертикальной ко­ ординаты, деленному на изменение горизонтальной координаты. У гори­ зонтальной линии изменение вертикальной координаты равно О, и, соответственно, ее наклон равен О. У линии с углом наклона 45° наклон ра­ вен 1, поскольку изменение вертикальной координаты равно изменению горизонтальной. У вертикальной линии наклон не определен , поскольку

1 65

Дин ам и ка м ате ри альн ых точек

О приводит к н­ ее горизонтальная координата не изменяется (деление на ывается по отсчит ние расстоя еопределенному результату). В этом случае ой оси нтальн горизо по ывается отсчит вертикальной оси (это Лх). Время (это Лt) .

наша оценка скорости накл ону = ЛХ / Лt

лt

=

Рис . 7 . 5 . Приближен ная оценка скорости

время

Возможно, вы уже сообразили, как получить более точную оценку скорости - нужно уменьшить Лt. Этот прием очень хорошо работает: по­ смотрите на рисунок 7.6.

Рис. 7.6. Чем меньше Лt, тем точнее оценка лt

вре мя

скорости

Можно повторять процесс раз за разом, уменьшая Лt и получая все более и более точные оценки мгновенной скорости. Этот метод мы будем использовать :во многих наших физических моделях. Решения можно сделать более точными, применяя более мелкие шаги. Вас, возможно, интересует, что произойдет в предельном случае, ког­ да Лt стремится к О. Этот случай записывается так:

v=

. 11m

л1 �0

Лх

­

Лt

Глава 7

1 66

Результатом этого выражения является как раз та мгновенная ско­ рость, которая нам нужна. Бесконечно малое Лt записывается как dt, а соответствующее ему бесконечно малое Лх - как dx . Используя эти обо­ значения , можно записать: Лх 1. dx v = 1m - = лно Лt dt

Величина dx / dt называется « производной от х по t � . Процесс вычис­ ления значения производной называется дифференцированием.

Зам ечание Это очень краткое и сжатое введение в дифферен циальное исч исление. Если вы хотите погрузиться глубже в математику, возьмите л юбую книгу по началам математического анализа. Существует множество хороших книг на эту тему. Можно порекомендовать, например, следующие: Gerald Brad­ ley, Karl Smith «Calculus» и Victor Bryant « Yet another introduction to analysis» ' .

Часто самого процесса дифференцирования можно избежать, приме­ нив хорошую численную аппроксимацию, легко реализуемую в коде.

Ускорен ие Помните машину во время торможения? Ее скорость постепенно падает, то есть изменяется с течением времени. Ускорение - это мера изменения скорости во времени, точно так же, как скорость - мера изменения прой­ денного расстояния во времени. Соответственно, среднее значение уско­ рения можно найти по формуле:

Лv а=Лt Здесь а есть среднее значение ускорения , Лv - изменение скорости, а Лt - интервал времени, за который произошло это изменение. Ускорение измеряется в м/с2 . Вот пример. Представим себе, что ваш спортивный ав­ томобиль может разогнаться от О до 32 м/с за 4 секунды. Тогда среднее ускорение за эти четыре секунды будет равно: а = (32 м/с) /

4 с = 8 м/с 2

А теперь попробуем сделать в некотором смысле обратную операцию. Предположим, что в течение 5 секунд вы набираете скорость с ускорени­ ем 4 м/с2 . Преобразовав уравнение 1

«Курс дифференциаль­ ного и интегрального исчисления» и Смирнов В. И. «Курс высшей математи­ ки» . Обе книги переиздавались много раз и выложены в Интернете, например, на сайте lib.mexmat.ru. - (Прим. перев.). Отечественному читателю доступнее Фихтенгольц Г. М.

1 67

Дин а м и ка м атери ал ьных точек Лv а=Лt

к виду v = aЛt

мы получим

v = ( 4 м/с 2)(5 с) =

20 м/с

Мгновенное значение ускорения можно получить, взяв лимит при Лt, стремящемся к нулю: а = lim

лно

Лv

=

Лt

dv dt

Мгновенное значение ускорения - это значение ускорения в конкрет­ ный момент времени.

Силы Большая часть древних мыслителей Запада считала, что движущиеся объ­ екты постепенно останавливаются. Это утверждение, на первый взгляд, подтверждается практикой. Если вы хоть раз передвигали мебель, вы го­ товы будете поклясться, что движение в данный момент времени не гаран­ тирует движения в последующие моменты. Однако у древних мыслителей были сомнения в правильности этой теории. Они видели несоответствие тео­ рии и практики и пытались выяснить причины этих несоответствий. Галилей предложил хорошую альтернативу этой теории, назвав ее принципом инерции. Его теория утверждает, что объект, движущийся по прямой линии с постоянной скоростью, будет продолжать двигаться веч­ но, если на него не действуют другие объекты. Создание такой теории по­ требовало хорошего воображения - почти все предметы вокруг нас постепенно останавливаются, если их постоянно не подталкивать. Одна­ ко Галилей понял, что это постепенное замедление движения вызвано трением между объектами, а не свойственно самим объектам. Сэр Исаак Ньютон позднее сформулировал ту же идею в более общей форме . Эта форма стала известна как первый закон Ньютона. Второй за­ кон Ньютона стал ответом на следующий логичный вопрос . Если объекты, на которые ничто не действует, будут двигаться с постоянной скоростью вечно, то, что же происходит с объектами, на которые что-то действует? Ответ - сила, действующая на объект, равна произведению массы этого объекта на ускорение, с которым он движется: F

= ma

У большинства объектов есть определенная масса - мера того, насколь­ ко сложно изменить скорость этих объектов . Чем тяжелее и массивнее

Глава 7

1 68

изменить его объект, тем большее усилие надо приложить к нему, чтобы т, что масса умевае подраз ла форму Эта ну. скорость на требуемую величи и. времен ем течени с объекта не изменяется тать силы, Например, с помощью этой формулы мы можем рассчи автомоби­ масса Но действующие на движущийся по дороге автомобиль. изменя­ она что ть, ля должна оставаться постоянной. Вы можете замети Однако . правы Вы ься. ется - автомобиль сжигает горючее, чтобы двигат играх. в ровать игнори это изменение малозаметное , и его можно было именн о в Большинство людей запоминают второй закон Ньютона практи­ на а полезн весьма ла виде формулы F ma, потому что эта форму мы Если . ние ускоре найти можем ке. Если у нас есть сила и масса, мы мы ть, скорос зная и, ть, скорос найти можем найти ускорение , мы можем яние. рассто можем вычислить пройденное =

За м е чание ельност и масса Возмож но, вы сл ышал и , что в специа льной теории относит Если только вы этом. об ойтесь объекта зависит от его скорост и . Не беспок ти в своей тельнос относи теорию льную специа ь не пытает есь модел ироват ся) , можно считать делает это й которо в игры, одной знаю ни не (я игре массу объекта не зависящ ей от его скорости .

на объект. Единственная сложность - учесть все силы, действующие аци­ гравит сил вида В соврем енной физик е считается, что есть четыре Теоре­ ия. действ взаимо о онные, электромагни тные, сильно го и слабог все, что будет тически, учтя все эти силы , мы сможем просчитать сделать. ожно невозм почти это ке практи происх одить с объектом. Но на несколь­ в объект на ующие действ силы, ем На практике мы измеря ие действ ающее описыв о хорош ние, уравне ем ких ситуациях, и выбира пру­ иваем растяг мы если мер, Напри ях. услови х этих сил в определенны - на нас дейст­ жину, то заметим, что она противодействует растяжению 7. 7. е рисунк на но вует сила, как показа

F

=

-

k (х - х0)

Хо Рис. 7.7. Сила в растянутой х

пружине

1 69

Дин а мика м атериальн ых точек Действующую на нас силу можно смоделировать уравнением

F = -k ( x

-

х0)

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

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

dx

vx = -

dt

Если х - расстояние, пройденное вдоль оси х в двумерной декартовой системе координат, то это уравнение описывает скорость движения мате­ риальной точки вдоль оси х, как показано на рисунке 7.8. Движение вдоль оси у можно рассматривать независимо от движения вдоль оси х : v

= dy у dt

Если нам нужно работать в трех измерения х, точно так же можно за­ писать и скорость движения вдоль оси z :

dz dt

vz = -

А теперь подумаем . Если

(vx, vy) , а вектор

два уравнения:

х

v - вектор, компоненты которого в 2D есть - вектор с компонентами (х , у), то можно объединить

v=

dx

-

dt

1 70

Глава 7

у

Рис. 7.8. х

Скорости вдоль осей

х

и у

В компонентной форме это будет выглядеть как

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

dt

dx

v= -

dxdt d [:'l� lidt dydt dt

Компонентная его форма будет такой:

-

у

Vz

dz

У вектора скорости, как и у любого другого вектора, есть величина и направление. Направление этого вектора - это направление движения материальной точки, а величина - скорость движения, как показано на рисунке 7. 9 :

1 71

Дин а м и ка м атери ал ьных точек скорость =

lvl = v

Рис . 7 . 9 . Вектор скорости материальной точки можно представить в виде направления и модуля скорости

Можно переписать в векторной форме все остальные уравнения , кото­ рые мы использовали в предыдущем разделе. Они будут выглядеть так: dx v= dt a=

F

dv

-

dt

= ma

Эти уравнения выражают скорость, ускорение и силу в виде векторов. Вы можете спросить: « Ну и что? >) Дело в том, что при программировании 3D-сцен, содержащих множест­ во движущихся объектов, часто можно просчитывать движение объектов, воспринимая их как материальные точки. Такое упрощение позволяет очень просто находить линейную скорость и ускорение каждого объекта, а также силы, действующие на них. Если скорости, ускорения и силы можно представлять в виде векторов, то их можно представлять и в виде матриц. И опять· вы можете спросить: « Ну и что? >) Вспомните, что все 3D-объекты в программах представляются в виде сетчатых моделей, представляющих собой совокупности вертексов. Когда объект двигается в 3D-сцене, двигаются все вертексы его модели. Это зна­ чит, что игра должна применять концепции скорости, ускорения и силы к каждому вертексу в модели. А ведь сложные модели состоят из сотен ты­ сяч треугольников - и каждый треугольник состоит из трех вертексов.

1 72

Глава 7

Как же программа может рассчитывать движение каждого вертекса в та­ ком сложном объекте? А она этого и не делает. Программа воспринимает объект как матери­ альную точку и использует векторы для представления сил, которые дей­ ствуют на этот объект, чтобы вычислить его скорость и ускорение. Затем эти скорость и ускорение представляются в виде матриц. После этого пе­ ремещение всех вертексов в объекте сводится к операции матричного ум­ ножения для каждого вертекса - и объект движется реалистично, так же, как и в реальной вселенной.

М одел и р ова ние ма те р иальн ых точе к Теперь, когда у нас есть физические соотношения, которые нам требова­ лись, мы можем написать код, моделирующий материальные точки в программе . Сначала мы создадим класс для представления материаль­ ных точек . Затем мы поместим материальную точку в среду, в которой нет ни силы тяжести, ни трения. Это позволит нам понаблюдать за пове­ дением материальных точек , на которые действует только одна внешняя сила. В последующих главах вы узнаете, как реализовать среды, в кото­ рых присутствуют сила тяжести и трение. Пример программы будет отображать шарик, движущийся слева на­ право по окну. Поскольку мы пока не рассматриваем кинематику враще­ ния, то вращаться в этой программе шарик не будет.

Кл асс dЗd_poi nt_mass Класс, представляющий материальную точку, должен хранить данные о массе этой точки, ее местоположении и действующих на нее силах. В лис­ тинге 7. 1 приведено определение такого класса - класса dЗd_point_mass.

Зам ечание Код примера программ ы из этой главы вы можете найти на компакт-диске, поставляемом с книгой. Он находится в папке Source\Chapter07 \Point­ Мas s . Есл и вы хотите п росто посмотреть на программу в работе, испол ня­ емый файл этой програм м ы находится в папке Source\Chapter07 \Bin.

Л истинг 7. 1 . Определение класса dЗd_poiпt_mass 1 2 З 4

class dЗd_point_mass

{ private : dЗd mesh objectМesh ;

Д и на м и ка м ате р иальных точек

1 73

5 6

scalar mas s ;

7

vector 3d centerOfМassLocation ; vector 3d linearVelocity ;

8 9

vector 3d linearAcceleration ; 10 vector З d swnForces ; 11 12 D 3DXМATRIX worldМatrix ; 13 1 4 puЫ ic : d3d_point_mass ( ) ; 15 16 17 bool LoadМesh ( s td : : string meshFileName ) ; 18 19 20 21 22 23 24 25 26 27 28 29

void Мas s ( scalar mas sValue ) ; scalar Мas s (void) ; void Location ( vector_3d locationCenterOfМa s s ) ; vector_3d Location (void) ; void LinearVelocity ( vector_3d newVelocity) ;

30 31

vector 3d LinearVelocity (void) ; _

32

void LinearAcceleration (

33 34

vector_3d newAcceleration) ; vector 3d LinearAcceleration (void) ; _

35 36 37 38 39

void Force ( vector_3d sumExternalForces ) ; vector_3d Force (void) ;

40 41 42 43 } ;

bool Update ( s calar change inTime ) ; bool Render (void) ;

В классе dЗd_yoint_ma s s определены рrivаtе-элементы данных, в которых хранятся сетчатая модель объекта, масса, координаты центра масс и характеристики движения. Кроме того, в нем есть специальный элемент данных, предназначенный для работы с Direct3D. Если вы по­ смотрите на строку 12 листинга 7 . 1 , вы увидите объявление элемента с именем worldМatrix. Это матрица глобального преобразования или гло­ бальная матрица, которая рассматривалась в главе 4 « 2D-преобразования и рендеринг � . Direct3D использует глобальную матрицу для обновления

Глава 7

1 74

расположения и ориентации объектов в трехмерном пространстве . Если в вашей программе используется множество объектов класса dЗd_po­ int mas s , движущихся по сцене, то для просчета движения каждого так ого объекта понадобится своя глобальная матрица. Именно она и хранится в элементе worldМatrix объекта и считывается из него при вы­ зове метода Update ( ) в программе. В методе Render ( ) эта матрица ис­ пользуется для просчета перемещения материальной точки. Кроме рrivаtе-элементов данных, в определении класса объявлены рuЬliс-методы этого класса (строки 1 5-42 листинга 7. 1). Большая часть этих методов просто считывает или записывает значения элементов дан­ ных. Основную работу в классе выполняют методы LoadМesh ( ) , Update ( ) и Render ( ) . Метод LoadМesh ( ) настолько прост, что объявлен как встраи­ ваемый в файле PМPointмas s . h. Код этого метода приведен в листинге 7 . 2. Листинг 7 . 2 . Метод Load Mesh( ) 1

inline bool dЗd_poin t_mas s : : LoadМesh (

2 з

s td : : string meshFileName )

4

assert (meshFileName . length ( ) >O ) ;

5 б 7

return

( objectмesh . Load (meshFileName) ) ;

Этот метод загружает сетчатую модель объекта, определяющую его внешний вид, с помощью метода dЗd_mesh : : Load ( ) .

Подска з ка В строке 4 листинга 7 .2 метод LoadМesh ( ) использует макрос assert ( ) чтобы гарантировать, что имя файла имеет ненулевую длину. Если про­ грамма вызовет этот метод, передав ему пустое имя файла, то выполнение программ ы аварийно завершится. Ошибка такого рода - это скорее ошиб­ ка программ иста, чем ошибка времени выполнения. Макрос assert ( ) га­ рантирует, что все ошибки такого рода будут устранен ы , п режде чем программа будет выпущена. Вообще говоря, он очень удобен для защиты от ошибок программ истов в параметрах вызова функций. ,

Методу Update ( ) приходится работать больше. Его код приведен в листинге 7 . 3 . Метод Update ( ) класса dЗd_point_mass просчитывает линейную динамику материальной точки. Пока он игнорирует существенные мо­ менты реального мира - вращение, трение и силу тяжести. Однако эта его версия позволит в будущем учесть все эти моменты. Метод Upd.ate ( ) начинается с проверки массы материальной точки. Она должна быть ненулевой. Это важная проверка, поскольку она позволяет обнаруживать часто встречающуюся ошибку программирования . Масса не должна быть нулевой или отрицательной - это невозможно физически.

Ди н а мика м атери ал ьных точек

1 75

Листинг 7 . 3 . Метод Update( ) 1 2

bool dЗd_point_mass : : Update ( scalar changeinTime)

3 4

//

5

/ / Начинаем просчет линейной динамики . //

6 7 8 9 10 11

/ / Находим линейное ускорение . // а = F/m assert (mas s ! =O ) ; linearAcceleration

=

sumForces /mas s ;

12 13

// Находим линейную скорость .

14

linearVeloci ty += linearAccelera tion * changeinTime ;

15 16

/ / Определяем новое местоположение центра масс .

17

centerOfМassLocation + = linearVelocity * changeinTime ;

18 19 20 21

// / / Просчет линейной дииаиики закончен . //

22 23 24

// Создаем матрицу преобразования . D3DxмatrixTranslation (

25

&worldМatrix ,

26

centerOfМassLocation . X ( ) ,

27 28

centerOfМassLocation . Z ( ) ) ;

29 30

centerOfМassLocation . Y ( ) ,

return ( true ) ;

31

П р едуп р еждение Выпол няя физическое моделирован ие, мы часто игнорируем массу неко­ торых объектов в системе. Это позволяет упростить вычисления настоль­ ко, чтобы их можно было выполнить. Если вы используете этот прием, не применяйте объекты класса d3d_point_mas s для реализации не имею­ щих массы объектов - они для этого не предназначены.

В строке 11 листинга 7. 3 метод Update ( ) преобразует формулу F = ma к виду а = F / m, чтобы найти ускорение материальной точки. В каждом кадре игры нужно просчитывать воздействие сил на материальную точку. Затем вызывается метод dЗd_point_mass : : Force ( ) для задания суммы всех сил. Когда программа вызывает метод dЗd_point_mass : : Update ( ) этот метод вычисляет реакцию материальной точки на воздействие силы. ,

Глава 7

1 76

Затем метод Update ( ) использует ускорение, чтобы найти новую линей­ ную скорость объекта в конце интервала времени, заданного параметром changeinTime . В строке 1 7 метод Update ( ) использует скорость (и интер­ вал времени), чтобы найти новое местоположение центра масс объекта. Вычисления в строках 1 1 , 14 и 1 7 весьма просты, поскольку мы исполь­ зуем для их вьmолнения инструменты, созданные в главе 3 «Математические инструменты� . Использование векторов делает формулы расчетов просты­ ми . Следующий mаг - преобразование в матричную форму. Оно необходи­ мо, чтобы программа могла использовать преобразования, рассмотренные в главах 4 и 5. Преобразование в матричную форму начинается в строке 24 листинга 7.3. В строках 24-28 метод Update () вызывает функцию Direct3D DЗDXМa.t­ rixTranslation ( ) , чтобы создать матрицу перемещения. Эта функция со­ храняет матрицу перемещения в матрице глобального преобразования объекта класса dЗd_point_mass. Матрица глобального преобразования ис­ пользуется при вызове метода dЗd_point_mass : : Render ( ) код которого приведен в листинге 7.4. ,

Листинг 7 .4. Метод Render( ) 1

bool dЗd_point_mass : : Render (void)

2 3

{

4

5 б 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

/ / Сохраня ем ма !I'рицу глобального преобразования . D 3DXМATRIX saveWorldМatri x ; theApp . D3DRenderingDevice ( ) ->GetTransform ( D3DTS_WORLD , &saveWorldМatrix) ; // Применяем :к о бtье :кту свою ма!I'рицу глобального // преобразования . theApp . D3DRenderingDevice ( ) ->SetTransform ( D3DTS_WORLD , &worldМatrix) ; // ВЪIП олняем рендеринг объе:кта с учетом выполненных / / преобразований . bool renderedOK=objectмesh . Render ( ) ; / / Восстанавливаем ма !I'рИЦУ глобального преобразования . theApp . D 3DRenderingDevice ( ) ->SetTransform ( DЗDTS_WORLD , &saveWorldМatrix) ; return ( renderedOK) ;

Для рендеринга одного объекта класса dЗd_point_mas s методу Render ( ) нужна только глобальная матрица для этого объекта. Эта

Дин а мика м атери ал ьных точек

1 77

матрица нужна, чтобы определить местоположение объекта в ЗD-про­ странстве . Однако в Direct3D может существовать одновременно только одна глобальная матрица. Поэтому методу Render ( ) приходится вы­ полнять следующие действия: 1.

2. 3.

4.

Сохранить ранее использовавшуюся глобальную матрицу. Выбрать глобальную матрицу объекта класса dЗd_point_mas s как используемую в данный момент. Выполнить рендеринг модели объекта класса dЗd_point_mas s . Восстановить ранее использовавшуюся глобальную матрицу, сохраненную в mаге 1 .

С помощью этих mагов метод Render ( ) позиционирует объект в ЗD-пространстве и выполняет рендеринг этого объекта, не повреждая ра­ нее использовавшуюся глобальную матрицу. Если бы метод Render ( ) не сохранял и не восстанавливал ранее использовавшуюся матрицу, то пере­ мещение, примененное к объекту класса dЗd_point_mass, применялось бы и к объектам, рендеринг которых выполнялся бы после рендеринга этого объекта. Результаты были бы как минимум нежелательными. В листинге 7.4 показано, что метод Render ( ) класса dЗd_point_mas s последовательно выполняет перечисленные в списке выmе действия. Сначала объявляется временная переменная для хранения ранее исполь­ зовавшейся глобальной матрицы. Затем с помощью функции Direct3D GetTransform ( ) глобальная матрица сохраняется в этой переменной. В строках 1 1 - 1 2 матрица, хранящаяся в объекте класса dЗd_point_mass, выбирается в качестве глобальной. Затем выполняется рендеринг - для этого в строке 16 вызывается метод dЗd_mesh : : Render ( ) . В строках 1 9-21 восстанавливается ранее использовавшаяся глобальная матрица.

Применен ие кл асса dЗd_poi nt_mass Итак, теперь у нас есть готовый к применению класс dЗd_point_mas s . Давайте используем его в программе. Но прежде чем мы это сделаем, я хочу немного поговорить об освещении в Direct3D. Мы воспользуемся не­ которыми возможностями Direct3D по моделированию освещения, чтобы получить в примере программы реалистично выглядящий шар.

ВКЛЮЧЕНИЕ МОДЕЛИ РОВАНИЯ ОСВЕЩЕНИЯ В DIRECTЗ D Игре можно заметно добавить реалистичности, используя возможно­ сти м оделирования освещения, присутствующие в Direct3D. Мы могли бы глубоко погрузиться в обсуждение поведения света в природе и мо­ делирования этого поведения в Direct3D. Но мы не будем этого делать. Хоть свет и физическое явление, он не рассматривается в данной кни­ ге. Мы занимаемся созданием реалистично ведущих себя , а не красиво выглядящих объектов.

Глава 7

1 78 Замечани е

Цвет, освещение и материалы - это тем ы , тесно связанные с текстуриро­ ванием. Чтобы игры выглядели профессионально сделан ными, разработ­ ч и ки должны разбираться во всех этих темах. Хорошее введение в н их например, книга Mason McCuskey «Special Effects Game Programming with DirectX» (издательство Premier Press).

Для наших целей нам достаточно знать, что DirectX обладает обширны­ ми возможностями по работе с освещением в ЗD-сценах. Он поддерживает несколько видов света - как от точечных источников, так и рассеянного. Пока нас интересует только рассеянный свет. Цвет объекта, который мы видим на экране, когда объект отображает­ ся с помощью Direct3D, определяется цветом материала объекта и цветом падающего на объект света. Если на объект нанесена текстура, то оказыва­ ет влияние и ее цвет. Но пока не будем пытаться разобраться подробнее. Шарик, отображаемый в примере программы, будет синего цвета. Все, что нам нужно, чтобы шарик был синим и круглым. Используя рассеянный свет, мы можем этого добиться. Поэтому давайте внесем в платформу не­ большие изменения, которые позволят нам использовать рассеянный свет. Сначала нужно изменить функцию Ini tDЗD ( ) в файле РМDЗDАрр . срр, чтобы она задействовала возможности моделирования освещения в Di­ rect3D. Посмотрите на исходный код в файле с компакт-диска. Откройте файл РМDЗDАрр . срр в папке Source\Chapter07\Pointмass и найдите в нем функцию Ini tDЗD ( ) . В конце этой функции есть такая строка: TheApp . dЗdDevice->SetRenderState (DЗDRS_LIGHTING , theApp . enaЬleDЗDLighting) ;

В предыдущих главах моделирование освещения отключалось, поэто­ му эта строка выглядела как theApp . dЗdDevice ->SetRenderState ( DЗDRS_LIGHTING , TRUE) ;

Теперь нужно передать эту информацию платформе в функции OnAp­ pLoad ( ) . Код этой функции приведен ниже в листинге 7 . 5 . Листинг 7 . 5 . Новая версия функции OnAppload ( ) 1

bool OnAppLoad ( )

2 З

{

4 5 б 7

8

/ / З адаем параметры инициализации о:киа . window_init__params windowParams ; windowParams . appWindowTitle = " Point Мass Test" ; windowParams . defaultx=l O O ; windowParams . defaul tY=lO O ; windowParams . defaul tWidth = 4 0 0 ;

1 79

Дин а мика м атери ал ьных точек 9

windowParams . defaultнeight

=

400 ;

10 11

/ / З адаем параме'l'рЫ иющиализации Direct3D .

12

d3d_ini t_params d3dParams ;

13 14

dЗdParams . renderingDeviceClearFlags

1 D3DCLEAR_ZBUFFER ;

=

D3DCLEAR_TARGET

15

d3dParams . surfaceBackgroundColor

16 17 18 19 20

d3dParams . enaЬleAutoDepthStencil true ; d3dParams . autoDepthStencilFormat = DЗDFМТ_D l б ;

21 22

theApp . InitApp (windowParams , d3dParams ) ;

23

return ( t:rue ) ;

D 3DCOLOR_XRGB ( 5 0 , 50 , 50 ) ; =

d3dParams . enaЬleD3DLighting

=

true ;

/ / Этот вызов ДОЛЖЕН присутствовать в этой функции .

24

Не забудьте, что функция OnAppLoad () необходима платформе. В этом примере программы она находится в файле PointмassTest . срр. Как и в главе 6 « Сетчатые модели и Х-файлы» программа передает параметры инициализации Windows и Direct3D с помощью структуры типа dЗd_init_params . В нее добавлено несколько новых элементов , по­ зволяющих задавать местоположение и размеры окна. Кроме того, один из новых элементов указывает, нужно ли включать или отключать моде­ лирование освещения при запуске программы. Вот определение новой версии структуры : struct d3d_init_params {

DWORD renderingDeviceClearFlags ; D3DCOLOR surfaceBackgroundColor ; bool enaЬleAutoDepthStencil ; D 3DFORМAT autoDepthStencilForma t ; bool enaЬleDЗDLighting ;

};

В строке 1 8 листинга 7 . 5 функция OnAppLoad ( ) устанавливает в true элемент этой структуры enaЬleDЗDLightning. Затем структура пе­ редается функции Ini tApp ( ) . Эта функция - элемент класса dЗd_app. Данный класс определен в файле РМDЗDАрр . h, который тоже находится в папке Source\ Chapter0 7 \ Pointмass на компакт-диске. Код функции Ini tApp ( ) приведен в листинге 7. 6 . Как видно и з листинга 7 . 6 , новая версия функции InitApp ( ) прос­ то копирует нужные данные из структуры в новые элементы класса dЗd_app . Взгляните на листинг 7 . 7 , в котором приведено новое опреде­ ление этого класса.

Глава 7

1 80 Листинг 7 . 6 . Новая версия функции lnitApp ( )

1 inline bool dЗd_app : : InitApp ( window_init_params windowParams , 2 d3d_init_params d3dParams ) 3 4 5 / / З адаем параметры инициалиsации окна б windowТi tle=windowParams . appWindowТitle ; defaul tx = windowParams . de faultx ; 7 defaultY = windowParams . defaul tY ; 8 9 defaultнeight = windowParams . defaul tHeight ; 10 defaultWidth = windowParams . defaul tWidth ; 11 12 / / З адаем параме'1'ры инициалиsации Direct3D . 13 deviceClearFlags = d3dParams . renderingDeviceClearFlags ; 14 backgroundColor d3dParams . surfaceBackgroundColor ; 15 enaЬleAutoDepthStencil = d3dParams . enaЫeAutoDepthStenci l ; =

16

17 18 19 20 21

autoDepthStencilFormat

=

dЗdParams . autoDepthStencilFormat ;

enaЬleD3DLighting = d3dParams . enaЬleD3DLighting ; appinitialized=true ; return ( appinitiali zed) ;

Л истинг 7 . 7 . Новое определение класса dЗd_app 1 2 3 4 5 б 7 8 9 10 11

class d3d_app { private :

/ / Свойства приложения . bool appinitialized ;

// Свойства охиа . std : : string windowТi tle ; int defaultx , de faultY ; int defaultHeight , defaul tWidth ;

/ / Свойства D i rect3D .

12 13 14

LPDIRECT3D9

15

LPDIRECT3DDEVICE 9

16 17 18

LPDIRECT3DVERTEXВUFFER9 vertexВuffer ; // Буфер для

19 20 21 22

direct3D ; 11 Используется дп я / / создания D3DDevice d3dDevice ; // Наше УС'1'рОЙСТВ 0

// рендериига

/ / хранения вертексов DWORD deviceClearFlags ; D3DCOLOR backgroundColor ; bool enaЬleAutoDepthStencil ; D3DFORМAT autoDepthStencilFormat ;

Дин а мика м атери ал ьных точек 23

181

bool enaЬleD3DLighting ;

24 25 puЫ ic : 26

d3d_app ( ) ;

27

bool Ini tApp (

28

window_init__params windowParams ,

29 30

d3d_init__params d3dParams ) ;

31 32

LPD IRECT3DDEVICE9 D3DRenderingDevice (void) ;

33 34

LPDIRECT3DVERTEXВUFFER9 D3DVertexВuffer (void) ;

35

void D3DVertexВuffer ( LPDIRECTЗDVERTEXВUFFER9 vertexВufferPointer) ;

36 37

DWORD RenderingDeviceClearFlags (void) ;

38

D3DCOLOR BackgroundSurfaceColor (void) ;

39 40

friend INT WINAPI AppМain (

41

HINSTANCE hins t ,

42 43

HINSTANCE , LPSTR , INT) ;

44 45 46

friend НRESULT InitD3D ( НWND hWnd) ;

47

friend VOID CleanupD3D ( ) ;

48 } ;

Просматривая листинг 7 . 7 , обратите особое внимание на строки 9, 1 0 2 3 . В них определены элементы, хранящие новую информацию, кото­ рая передается через функцию Ini tApp ( ) . Как я уже упоминал ранее, эта информация используется в функции Ini tDЗD ( ) . Теперь, включив моделирование освещения в Direct3D, можно при­ ступить к моделированию движения шарика. Вернемся к обсуждению использования класса dЗd_point_mass в физическом моделировании. и

И Н ИЦИАЛ ИЗАЦИЯ ОБЪЕКТА КЛАССА DЗ D_POINT_MASS Если вы просмотрите функцию Gameinitialization ( ) в файле Point­ мassTest . срр, то увидите, что она инициализирует объект класса dЗd_po­ int_mass. Объявление этого объекта выглядит так: d3d_point_mass theObj ect ;

Оно расположено в начале файла. Взгляните на листинг 7.8, чтобы увидеть, как в функции Gameiniti­ al ization () инициализируется объект класса dЗd_point_mass.

1 82

Глава

7

Листинг 7.8. Функция Gamelnitialization() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

bool Gameinitial ization ( ) {

// Загружаем модель шарика . theObj ect . LoadМe sh ( "bowlball . x" ) ;

// Задаем начальное местоположение шари�а . theObject . Location (vector_Зd ( - 5 . 0 f , 0 . 0 , 0 . 0 ) ) ;

/ / Задаем его массу . theObject . мass ( l O ) ;

// / / Настраиваем напразленный рассеянный свет . // D3DLIGHT9 l ight ; ZeroMemory ( & l ight , sizeof ( l ight) ) ; light . Type = D3DLIGHT_D IRECTIONAL ; D3DXVECTOR3 vecDir ; vecDir = D3DXVECTOR3 ( 0 . 0f , - 1 . 0 f , l . Of) ; D3DXVec3No:nuali ze ( ( DЗDXVECTOR3 * } &light . Direction , &vecDir ) ;

/ / Задаем цвет рассеянного света light . Diffuse . r 1 .0f; l ight . Diffuse . g = 1 . 0f ; light . D iffuse . b = 1 . 0f ; light . Diffuse . a = l . Of ; theApp . D3DRenderingDevice ( ) ->SetLight ( О , & l ight ) ; theApp . D3DRenderingDevice ( ) ->LightEnaЬle ( О , TRUE ) ; theApp . D3DRenderingDevice ( ) ->SetRenderState ( D3DRS_D IFFUSEМATERIALSOURCE , D3DMCS_МATERIAL) ; =

return

( true ) ;

Эта версия функции Gameini tialization ( ) выполняет три основные задачи. В строке 4 листинга 7.8 она загружает сетчатую модель объекта, представляемого материальной точкой. Это модель шара для боулинга, хранящаяся в файле bowlball . х. Этот файл хранится в папке программы Source\Chapter0 7 \ Pointмass на компакт-диске. После этого функция: Gameini tialization ( ) задает свойства мате­ риальной точки. Она определяет начальное местоположение шарика слева от окна программы. Поэтому вначале mарик в окне не виден . Кро­ ме того, задается масса шарика. Она равна 10 кг (22 фунта) - довольно много для боулинг-шара.

1 83

Дин а мика м атери ал ьн ых точек

И, наконец, функция Gameinitial ization ( ) настраивает рассеянное освещение для Direct3D. В строке 1 5 объявлена переменная типа D3DLIGHT 9 . Ее содержимое обнуляется вызовом функции Windows Zero­ Memory ( ) . В строке 16 источник света делается направленным. В строках 19-21 для определения вектора, задающего направление света, использу­ ется переменная типа DЗDXVECTORЗ . В строках 24- 27 задается белый цвет света. В строке 28 Direct3D отдается указание использовать созданный ис­ точник света, а в строке 29 включить этот источник . В строках 30-32 со­ вмещением цветов света и материала шарика получается цвет, который будет отображаться на экране . Ну что ж, все готово. У нас есть материальная точка с загруженной моделью . У нас есть источник освещения. Вероятно, можно было бы от­ пустить какую-нибудь кинематографическую шутку, но, к сожалению, я не могу вспомнить подходящую. Поэтому, пожалуйста, просто продол­ жайте читать. -

ОБНОВЛЕНИЕ М ЕСТОПОЛОЖЕНИЯ ОБЪЕКТА КЛАССА DЗ D_POINT_MASS В ходе выполнения программы нужно заново вычислять местоположение материальной точки при рендеринге каждого кадра анимации . Как и в предыдущих главах, это делается в функции UpdateFrame ( ) Чтобы увидеть ее исходный код, взгляните на листинг 7.9. .

Листинг 7 . 9 . Функция UpdateFrame ( ) 1 2 3 4 5 б 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

bool UpdateFrame ( )

{ // Создаем ма'l'рицу отображения - как в предыдущих примерах . D3DXVECTOR3 eyePoint ( O . Of , 3 . 0f , - 5 . 0 f) ; D3DXVECTOR3 lookatPoint ( O . O f , 0 . 0f , 0 . 0 f) ; D 3DXVECTOR3 upDirection ( O . Of , l . Of , 0 . 0f ) ; D 3DXМATRIXA1 6 viewмatrix ; DЗDxмatrixLookAtLН ( &viewМatrix , &eyePoint , &lookatPoint , &upDirection ) ; theApp . D 3DRenderingDevice ( ) - >SetTransform (D3DTS_VIEW, &viewМatrix) ;

// Создаем ма'l'рицу проецирования - как в предыдУЩИХ примерах . D 3DXМATRIXA1 6 projectionмatrix ; D3DXМзtrixPerspectiveFovLH ( &projectionмatrix , D3DX_P I / 4 , l . Of , 1 . 0 f , 1 0 0 . 0 f ) ; theApp . D 3DRenderingDevice ( ) ->SetTransform (D3DTS_PROJECTION , &proj ectionМзtrix) ; // // В течение одного интервала времени приIСJiадываем к шарику // сипу .

Глава 7

1 84 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

/ / Эта инициализация выполняется только один раз . static bool forceApplied = false ;

/ / Если сила еще не прикладывалась . . . if

( ! forceApplied)

// Прикла дыв аем CИJiy . the0bj ect . Force (vector_3d ( 2 . 0 f , 0 . 0 , 0 . 0 ) ) ; forceApplied = true ; } / / Иначе сила уже прикладывалась . . . else

/ / Присваиваем ей нулевую величину . the0bj ect . Force (vector_3d ( 0 . 0 , 0 . 0 , 0 . 0 ) ) ;

/ * Задайте napaмeirpy значение меж,цу О и 1 для более плавной анимации . * / theObject . Update ( l ) ; return ( true ) ;

Функция UpdateFrame ( ) начинается так же, как и аналогичные функции в предьrдущих главах . Сначала она подготавливает матрицы, требующиеся Direct3D. Когда эти матрицы готовы, она один раз прикла­ дывает к шарику силу, чтобы он начал двигаться. Чтобы сделать это, функция использует статическую переменную. Возможно, вы знаете, что в С++ статические переменные в функциях инициализируются один раз - при первом вызове этих функций. После этого инициализапия никогда не выполняется во второй раз. Использова­ ние статической переменной позволит нашей функции определить, прикла­ дывалась ли уже к шарику сила. Если нет, то переменная forceApplied будет установлена в значение false. Поэтому оператор if, начинающийся в строке 27, задаст силу, действующую на шарик (строка 30). Кроме того, он установит переменную forceApplied в значение true.

Замечание Обратите в н имание, что сила действует в направлении увел ичения значе­ ний по оси х. Поэтому шарик будет двигаться по экрану слева направо. Из­ н ачально он находится слева от области, видимой в окне програ м м ы . Когда программа запустится, о н переместится п о окну программы и исчез­ нет за его правым краем. После этого н ичего нового в окне не отобразит­ ся, поэтому его можно закрыть.

Дин а мика м атер и ал ьных точек

1 85

При следующем вызове функции UpdateFrame ( ) переменная for­ ceAppl ied сохранит свое значение - true, поэтому выполнится блок выражений в операторе else. Этот блок выражений уменьшит силу до нуля, поэтому шарик получит толчок только при первом выполнении функции Upda teFrame ( ) . После этого на него не будут действовать ни­ какие силы. Шарик будет двигаться бесконечно , пока выполняется программа. Приятно видеть, что наша имитация работает в соответст­ вии с законами Ньютона. Это свидетельствует о том, что мы правильно составили программу. Последнее, что делает функция Upda teFrame ( ) - вызывает для объекта-шарика метод dЗd_point_mas s : : Update ( ) . Этот метод пере­ считывает местоположение шарика, исходя из действующих на шарик сил (если таковые есть) , скорости шарика и его ускорения. Если хотите посмотреть еще раз на код метода dЗd_point_mas s : : Update ( ) , он приведен в листинге 7 . 3 .

Р Е НДЕРИ Н Г ОБЪЕКТА КЛАССА DЗD_POINT_MASS Рендеринг объекта класса dЗd_yoint_mass - действительно сложная про­ цедура. Ужасно сложная. Но если вы еще не испугались, то можете посмот­ реть на код, выполняющий этот рендеринг. Он приведен в листинге 7.10. Листинг 7. 1 О. Рендеринг объекта класса dЗd_poi nt_mass 1

bool RenderFrame ( )

2

{

З 4

theObject . Render ( ) ; return ( true ) ;

5

Впечатляет? Хм. Ладно, я пошутил. Если вы недавно занимаетесь компьютерной графикой, то вряд ли сможете представить себе, как это здорово - иметь возможность выполнить рендеринг сложного объекта вроде шарика с помощью такой короткой функции . Я начинал работать с компьютерной графикой больше 20 лет назад. Тогда, чтобы вывести что-нибудь на экран, мне приходилось писать собственный модуль ренде­ ринга. Этот модуль должен был быть написан на ассемблере . Если вы не знаете , что это значит, можете считать, что вам повезло. Да, в те годы жизнь иногда была чертовски сложной. В любом случае, из листинга 7 . 1 0 видно, что функция RenderFrame ( ) просто вызывает метод Render ( ) класса dЗd_yoint_mas s для объекта theObj ect. Этот метод, в свою очередь, вызывает метод класса dЗd mesh, чтобы применить глобальную матрицу, созданную методом Up� te ( ) класса dЗd_poi nt_mass. Кроме того, вызывается метод dЗd_mesh : : Ren­ der ( ) , чтобы выполнить рендеринг сетчатой модели. Поскольку эти мето­ ды выполняют все нужные действия, на долю функции RenderFrame ( ) остается немногое. Другими словами, если м ы используем объект класса

1 86

Гл ава 7

d3d_.Point_mass, нам нужно настроить его, приложить к нему силу и по­ зволить ему двигаться. Больше ничего делать не нужно. Неплохо, правда?

М а териал ьн ые точ ки в и г рах Часто ли используются материальные точки в играх? Почти постоянно. И чем дальше, тем чаще они используются . С увеличением вычислитель­ ной мощи компьютеров важность материальных точек в играх будет воз­ растать. Позвольте привести пример. Предположим, что в игре персонаж может стрелять в стену. В таких иг­ рах не обязательно использовать материальные точки. Когда пуля попадает в стену, игра применяет к стене текстуру, отображающую щербину в сте­ не и обесцвечивание от пороха. Но на самом деле стена не повреждается. Это неплохо, но в последнее время игры становятся более реалистичными. В некоторых играх используются системы маленьких частиц, чтобы изобразить фрагменты стены, отлетающие от нее при попадании. Эти фрагменты исчезают по прошествии некоторого времени. Однако на са­ мом деле стена все равно не повреждается. Если у нас есть достаточные вычислительные ресурсы , мы можем смоделировать появление дыр в стене при попаданиях . Программе при­ дется отслеживать, с какой силой пуля врезается в стену. Если игрок стреляет в стену издалека, пуля не должна пробить стену. Если стрельба ведется в упор, то в стене должны появляться дыры. Чтобы моделировать такое поведение, нужно использовать материальные точки. Сложные объекты часто представляются в виде наборов материаль­ ных точек . Если мы создадим объект , состоящий из тяжелых частей , сое­ диненных более легкими, его можно будет представить в виде набора материальных точек. Если это сделать, то объект можно красиво разру­ шить. Например, при взрыве могут разлететься в разные стороны части объекта. Остается добавить зрелищные эффекты взрыва, и все будет вы­ глядеть очень реалистично .

И то г и Вот и все о кинематике материальных точек. Мы далеко продвинулись! Начав с основных понятий о материальных точках и скоростях, мы создали систему, позволяющую моделировать материальные точки, и применили ее. Мы еще не затрагивали силу тяжести, отскок объектов при столкнове­ ниях, трение и сцепление, но скоро мы это сделаем. Пока у нас есть весьма реалистичная модель шарика, и этого достаточно.

Гла ва 8

Стол к новен и я м ате риал ьн ых то ч е к В созданной нами в предыдущей главе модели материальные точки могут двигаться под воздействием приложенных к ним сил. Мы можем добавлять в модель все новые силы, делая ее все более реалистичной (и сложной), но нам по-прежнему будет не хватать одной важной вещи: материальные точ­ ки не взаимодействуют между собой. В этой главе мы займемс я устране­ нием данного недостатка. Но прежде чем мы займемс я моделированием столкновений, нужно уяснить: это моделирование на самом деле состоит из двух отдельных проблем. Первая - обнаружение столкновений. Чтобы игра могла отреа­ гировать на столкновение, она должна знать, что столкновение произош­ ло . Эта проблема может показатьс я простой, но на самом деле это совсем не так . Обнаружение столкновений - сложная задача. В любом случае - обнаружение столкновений есть задача программи­ рования , а не физическая задача, поэтому она не рассматриваетс я в этой книге подробно . Мы только кратко рассмотрим основные методы обнару­ жения столкновений и перейдем ко второй проблеме, св язанной со столк­ новениями : реакции на столкновения . Реакци я на столкновения - это физическая задача. Под реакцией на столкновения подразумеваетс я поведение сталкивающихс я объектов в момент столкновения и после него. Это поведение определ яетс я физикой. После обзора методов обнаружени я столкновений мы займемс я модели­ рованием реакции на столкновения для материальных точек.

Об наруж ение стол кновени й Научные центры полны дипломированных с пециалистов, п ытающихся создать новые, более быстрые и эффективные алгоритмы обнаружения столкновений, поэтому на эту тему есть горы литературы, в которых вы м ожете (при желании) копаться всю оставшуюс я жизнь. В компьютер­ ных играх самые лучшие решения - обычно самые простые, поэтому мы рассмотрим только самые основные методы обнаружения столкновений.

Глава 8

1 88

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

Рис. 8. 1

.

Объект с ограничивающей сферой

Например, этот метод можно применить для поиска столкновений объектов с плоскостью земли. Вот последовательность действий: 1. 2.

3.

Проверим, находится ли объект над плоскостью. Если да, пере­ ходим к шагу 2 . Сравним расстояние от материальной точки до поверхности и радиус ограничивающей сферы этой материальной точки. Если радиус больше этого расстояния, произошло столкновение, по­ этому переходим к шагу 3 . В противном случае пропускаем шаг 3 и повторяем шаги 1 и 2 для следующего объекта в сцене. Объект столкнулся с землей. Просчитать реакцию на столкно­ вение.

Этот метод работает не только для земли, но и для любой другой плос­ кой поверхности. На рисунке 8 . 2 показано, как можно применить его для обнаружения столкновений со стеной . Рассматриваемые объекты не обязательно должны быть шариками. Они могут быть любой формы, просто их нужно поместить в ограничива­ ющие сферы. Вариацию этого алгоритма можно применить в игре для обнаруже­ ния столкновений материальных точек между собой, как показано на рисунке 8 . 3 . Чтобы определить, произошло л и столкновение между объектами, нужно знать радиусы ограничивающих сфер и векторы местоположений этих объектов. Затем нужно сравнить сумму радиусов сфер и расстояние

1 89

Стол кно в ения м атери ал ьных точек

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

Рис. 8 . 2 . Обнаружен ие столкновений

шарика со стеной

Рис. 8.3. Столкновение двух

объектов, представленных

огран ичивающими сферами радиус объекта

1

радиус объекта 2

расстоя ние столкновения нет

радиус объекта

1

радиус объекта 2

расстояние стол кновение

Рис. 8.4. Сравнение расстояния между объектам и и суммы их

радиусов

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

1 90

Гл ава 8

есть координаты центров шаров, представленные в виде векторов, то рас­ стояние между ними можно найти, вычтя один вектор из другого . Это де­ монстрирует рисунок 8 . 5. Вычтя вектор р 1 из вектора р2 с рисунка 8. 5 , мы получим вектор рас­ стояния между двумя центрами масс. Теперь программе осталось только найти длину вектора расстояния и сравнить ее с суммой радиусов огра­ ничивающих сфер. Если эта длина меньше суммы радиусов, значит, произошло столкновение. Если нет, столкновения нет.

Рис. 8 . 5 . Нахожден и е вектора расстоян ия м ежду двумя объектам и

Если описанные выше действия не совсем понятны, посмотрите на следующий фрагмент кода. Он использует два объекта класса dЗd_.PO­ int_mass - balll и Ьа1 12 - и вычисляет расстояние между ними как длину вектора расстояния. После этого он сравнивает найденное расстоя­ ние и сумму радиусов . 11 Вычисляем разницу векторов местоположения сфер . vector Зd distance = ball l . Location ( ) - bal1 2 . location ( ) ; 11 Находим расстояние между центрами сфер (длинУ вектора расстояния) scalar magnitude = distance . Norm ( ) ; 11 Вычисляем расстояние , при котором произойдет столкновение . scalar minDistance = (balll . Radius ( ) + Ьа1 1 2 . Radius () ) ; if (magnitude < minD istance ) {

11 Произошло столкновение . Нужно смоделировать реаlСЦИIО на него .

Этот алгоритм прекрасно работает , но у него есть очень существенный недостаток: он очень медленный! Вспомните - нужно проверять, произош­ ло ли столкновение между каждой парой частиц в сцене. И при каждой проверке нужно вычислять квадратный корень. Это очень медленная операция - она выполняется почти в 70 раз дольше, чем операция умно­ жения двух чисел с плавающей запятой. Вспомните код метода vec­ tor_Зd : : Norm ( ) , вычисляющего норму вектора:

Стол кно вения м ате р и ал ьных точек

1 91

inline scalar vector_Зd : : Norm (void) {

return ( sqrtf (x*x + у*у + z * z ) ) ;

Ускорить работу этого алгоритма можно несколькими способами. Можно, например, использовать быстрые способы приближенного вы­ числения квадратных корней. Но внимательные читатели, вероятно , предложат еще лучший способ: вообще не вычислять квадратный корень! Если у нас есть два положительных числа х и у, то, если х больше у, то и х 2 будет больше у2 , правда? Так почему бы нам ни сравнивать квад­ рат расстояния с квадратом суммы радиусов вместо расстояния с суммой радиусов? Вот код, в котором реализована эта идея: / / Вычисляем разницу векторов местоположения сфер . vector_Зd distance = bal l l . Location ( ) - ball2 . location ( ) ; // Находим расстояние между центрами сфер ( длину вектора / / расстояния) . scalar magnitude = distance . NormSquared ( ) ; / / вычисляем минимальное расстояние , при котором столкновение / / не произойдет . scalar minDistance = (ball l . Radius ( ) + ball2 . Radius ( ) ) ; // Возводим расстояние в квадрат . minDistance *= minDistance ; if (magnitude < minDistance ) {

/ / Произошло столкновение . Нужно смоделировать реакцию на него .

В этом фрагменте используется метод vector_Зd : : NormSquared ( ) , по­ хожий на vector_Зd : : Norm ( ) , но не вычисляющий квадратные корни: inline scalar vector_З d : : Norm (void) {

return ( ( x*x + у*у + z * z ) ;

В новом варианте алгоритма есть дополнительная операция умноже­ ния чисел с плавающей запятой - она нужна для возведения суммы ради­ усов в квадрат . Однако эта операция выполняется молниеносно по сравнению с вычислением квадратного корня. Использование ограничивающих сфер - часто лучший способ обна­ ружения столкновений . Этот способ прост, быстро работает и позволяет получать прекрасные результаты для многих задач . Если вы хотите ис­ пользовать более изощренные способы обнаружения столкновений, все же попробуйте сначала использовать способ с ограничивающими сфера­ ми. Возможно, его будет достаточно, и не придется дополнительно нагру­ жать процессор, используя более изощренные способы.

1 92

Гл ава 8

О гра ничивающи е цил и ндры Вместо сфер можно ограничивать объекты некоторыми более сложными фигурами. Ограничивающие цилиндры очень удобно применять в играх, когда большинство объектов не изменяет свою ориентацию относительно определенной поверхности. Хорошие примеры таких игр - Doom и похо­ жие на него стрелялки, в которых большинство персонажей не пригиба­ ется, даже оказавшись под ураганным огнем. Все персонажи сохраняют постоянную ориентацию относительно пола. На рисунке 8 . 6 показано применение ограничивающего цилиндра. Чтобы обнаружить столкновения, нужно проверять пересечения верх­ него и нижнего срезов цилиндра, а не только его боковой поверхности. Первый шаг к реализации такого подхода - добавление элементов данных, нужных для хранения размеров цилиндра, в класс dЗd_yoint_mass. На ри­ сунке 8. 7 показаны эти размеры относительно местоположения объекта.

Рис. 8 . 6 .

Объект с ограничивающим цилиндром

Ради ус ,..-,

]высот Рис. 8.7. Размеры огран ичивающего

цилиндра

Чтобы выяснить , столкнулись ли два цилиндра, нужно выполнить два действия. Предположим для начала, что цилиндры всегда ориентиро­ ваны так, как показано на рисунке 8 . 6 и 8. 7. Это позволит нам превратить задачу из трехмерной практически в двумерную . Радиус ограничиваю­ щих цилиндров будет всегда лежать в плоскости x z . Поэтому первый шаг - рассматривать цилиндры как окружности в плоскости x z . Найдем

1 93

Столкновения м атери ал ьных точек

расстояние между центрами этих окружностей. Если это расстояние боль­ ше суммы радиусов окружностей, значит, столкновения нет, и следую­ щий шаг выполнять нет необходимости. Если расстояние меньше этой суммы , то столкновение возможно, и нужно выяснить, произошло ли оно. Для этого служит следующий шаг. Следующий шаг - выяснить, есть ли пересечение цилиндров по высо­ те, как показано на рисунке 8 . 8 . Если верхний край одного и з цилиндров находится н а высоте между нижним и верхним краями другого цилиндра, то столкновение произош­ ло, и программа должна на него отреагировать. Если нет, то столкнове­ ния нет, несмотря на то, что радиусы цилиндров перекрываются. Это может происходить, например, если персонажи находятся друг над дру­ гом на разных этажах здания.

1

� -- -- +----_.

-

-

-

-

-

-

-

-

-

..._

Рис. В . В . П роверка пересечения цилиндров в вертикальной плоскости

Огра ничивающие блоки Теперь, познакомившись с ограничивающими сферами и ограничивающи­ ми цилиндрами, вы, вероятно, не удивитесь, узнав, что можно использо­ вать для обнаружения столкновений и прямоугольные блоки, вроде показанного на рисунке 8 . 9 . Их можно использовать как в двумерных, так и в трехмерных системах координат.

Рис. В . 9 . Ограничивающий

блок

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

1 94

Глава 8

Взгляните на рисунок 8. 10, на котором: изображен плоский многоуголь­ ник. Хотя ограничение окружностью работает, оно перекрыв ает боль­ шую площадь, не относящуюся к мноrоугольнику. В этом случ:ае лучше применить ограничение прямоугольником.

Рис. 8. 1 О. Выбор ограничивающе й фигуры, оптимальной длS1 объекта

Выяснить, произошло ли столкновение двух прямоугольных блоков , можно следующим образом . Выберите вертекс одного и з блоков. Затем проверьте, находится ли этот вертекс вflутри другого блока. Если да, зна­ чит, произошло столкновение, как по1= xl & & х = yl & & у = zl & & z SetLight ( О , &light ) ; theApp . D3DRenderingDevice ( ) - >LightEnaЬle ( О , ТRUE ) ; theApp . D3DRenderingDevice ( ) - >SetRenderState ( D3DRS_D IFFUSEМATERIALSOURCE , D 3DMCS_МATERIAL) ; return ( true ) ;

Эта версия функции Gameinitiali zation () начинается с загрузки сетчатой модели первой материальной точки. Используется та же модель шара для боулинга, что и в главе 7. Масса шарика задается равной 10 ки­ лограммам. В строке 10 листинга 8 . 2 коэффициент восстановления ша­ рика задается равным 0 . 9 - это намного больше, чем у реального шара для боулинга.

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

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

Стол кно в ения м ате р и ал ьн ых точек

21 1

Далее функция Gameini tializ ation ( ) задает начальное местополо­ жение первого шарика - он расположен за левым краем окна программы. В строке 22 задается начальное местоuоложение второго шарика - этот шарик расположен за нижним краем окна. В строках 25-26 задаются на­ чальные силы , под воздействием которых шарики начинают двигаться к началу координат (центру окна) с одинаковой скоростью. Это гарантиру­ ет, что произойдет столкновение. Оставшаяся часть функции Gameinitializ ation ( ) настраивает осве­ щение - так же, как это делалось в главе 7 .

ОБНОВЛЕН И Е КАД РОВ Функция Upda teFrame ( ) теперь должна будет проверять, нет ли столк­ новений между движущимися шариками. Если столкновение произош­ ло, она должна будет вычислить силы, действующие на шарики. Эти вычисления я выделил в отдельную функцию, которая рассматривается в следующем разделе. А пока посмотрите на листинг 8.3, в котором приведе­ на версия функции UpdateFrame ( ) , обнаруживающая столкновения. Листинг 8 . 3 . Функция UpdateFrame( ) , обнаруживающая столкновения 1 bool UpdateFrame ( ) 2 { / / Создаем матрицу отображения - как в предыдущих примерах . З D ЗDXVECTORЗ eyePoint ( O . Of , 3 . 0f , - 5 . 0 f ) ; 4 D3DXVECTOR3 lookatPoint ( O . O f , O . Of , O . O f) ; 5 D 3DXVECTOR3 upDirection ( O . Of , l . Of , O . Of) ; 6 D3DXМATRIXA16 viewМatrix ; 7 D3DXМatrixLookAtLН ( &viewМatrix , &eyePoint , &lookatPoint , 8 9 &upDirection) ; theApp . D3DRenderinqDevice ( ) -> 10 11 SetTransform (D3DTS_VIEW , &viewМatrix) ; 12 // Создаем матрицу проецирования - как в пре.цы.цущих примерах . 13 D3DXМATRIXA1 6 projectionМatrix ; 14 15 D 3DXМ&trixPerspectiveFovLН ( &projectionМatrix , D3DX_PI/4 , 1 . 0 f , 1 . 0 f , 1 0 0 . 0f) ; 16 theApp . D3DRenderinqDevice ( ) 17 - >SetTransform (D3DTS_PROJECTION , &projectionМatrix) ; 18 19 / / Эта инициализация выполняется только один раз . 20 static bool forceApplied false ; 21 static vector_3d noForce ( 0 . 0 , 0 . 0 , 0 . 0 ) ; 22 23 / / Если силы е ще н е приJСJiадывалась . . . 24 if ( ! forceApplied) 25 26 forceApplied true ; 27 =

=

212 28 29 30 31 32 33 34 35 36 37

38 39 40 41 42 43 44 45 46

47 48 49 50 51 52 53 54 55 56 57 58 59

Глава 8 } / / Иначе с илы уже прикладывалась . . .

else // Делаем их нулевЪIМИ .

allParticles [ O ] . Force (noForce) ; allParticles [ l ] . Force (noForce) ;

// / / Проверяем , есть ли стол:кновения .

// / / Находим вектор расстояния между шариками . vector 3 d distance allParticles [ O ] . Location ( ) - allParticles [ l ) . Location ( ) ; scalar distanceSquared distance . NormSquared ( ) ; =

=

/ / Находим квадрат суммы радиусов шариков . scalar minDi stanceSquared = allParticles [ O ] . BoundinqSphereRadius ( ) + allParticles [ l ) . BoundinqSphereRadius ( ) ; minDistanceSquared *= minDistanceSquared ; // ИЗИеняйте значения между О и 1 , чтобы добиться плавной анииации . scalar timeinterval = 1 . 0 ; / / Если произошло столкновение . . . if (distanceSquared < minDistanceSquared) / / Отреагировать н а столкновение .

HandleCollision ( di stance , timeinterval ) ; }

60

61 62 63

allParticles [ O ] . Update ( timeinterval) ; al lParticle s [ l ] . Update ( timeinterval) ;

64

return ( true ) ;

65

В строках 4 - 1 8 листинга 8.3 функция UpdateFrame ( ) выполняет стан­ дартные операции, необходимые для работы с Direct3D. В строке 21 объяв­ ляется статическая переменная forceApplied, которая используется так же, как в главе 7 . В строке 22 объявляется статическая переменная no­ Force типа vector_Зd, которая используется для инициализации векто­ ров сил в строках 33-34. Поиск столкновений начинается в строке 4 1 . Чтобы определить , произошло л и столкновение, функция Upda­ teFrame ( ) вычисляет квадрат расстояния между центрами шариков . Это делает код из строк 4 1 -4 3 . Далее в строках 46-48 вычисляется сумма

Стол кновения м атери ал ь ных точек

213

радиусов шариков , которая в строке 49 возводится в квадрат . В строке 55 полученные значения используются, чтобы определить, произошло ли столкновение. Если столкновение произошло, то в строке 58 вызывается функция HandleCollision ( ) . Как вы вскоре увидите, эта функция вы­ числяет силы, действующие на материальные точки. Когда в строках 61-62 вызывается метод dЗd_point_mass : : Update ( ) , на шарики дейст­ вуют силы столкновения.

ОБРАБОТКА СТОЛКНОВЕНИ Й Силы, возникающие при столкновении , вычисляет функция Handle­ Col l i s ion ( ) . Это вспомогательная функция, которая не обязательно должна присутствовать в платформе физического моделирования. Код этой функции содержится в файле ParticleBounce . срр . Он приведен в листинге 8 . 4 . Листинг 8.4. Функция HandleCollision ( ) 1 2 3 4 5 6 7

8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

void HandleCollision ( vector_3d separationDis tance , scalar chanqeinTime ) // // Находим скорости объектов после столкновения . // / * Сначала нормализуем вектор расстояния , поскольку он перпендикулярен к столкновению . * / vector 3d uni tNormal = separationDistance . Normalize ( FLOATING_POINT_TOLERANCE) ; /* Вычисляем проекции скоростей в направлении , перпендикУлярном направлению столкновения . * / scalar veloci tyl allParticles [ O ] . LinearVelocity ( ) . Dot (unitNormal) ; scalar velocity2 = allParticles [ l ] . Linearveloci ty ( ) . Dot (unitNormal ) ; =

/ / Находим средний коэффициент восстановления . scalar averaqeE = ( al lParticle s [ O ] . Elasticity ( ) * allParticles [ l ] . Elasticity ( ) ) / 2 ; / / Вычисляем скорости после столкновения . scalar finalVelocityl ( ( ( al lParticles [ O ] . Мas s ( ) (averaqeE * allParticles [ l ] . Mass ( ) ) ) * velocityl ) + ( ( 1 + averaqeE) * allParticles [ l ] . Мass ( ) * velocity2 ) ) / (allParticles [ O ] . Мas s ( ) + allParticles [ l ] . Мass ( ) ) ; scalar fina1Velocity2 = =

214 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55

Глава 8 ( ( (allParticles [ l ] . Мass ( ) (averageE * allParticles [ O ] . Мass ( ) ) ) * velocity2 ) + ( ( 1 + averageE) * allParticles [ O ] . Мass ( ) * velocityl ) ) / (allParticles [ O ] . Мass ( ) + allParticles [ l ] . Мass ( ) ) ; allParticles [ O ] . LinearVeloci ty ( ( finalVelocityl - velocityl) * unitNormal + allParticles [ O ] . LinearVelocity ( ) ) ; al lParticles [ l ] . LinearVelocity ( ( fina1Velocity2 - velocity2 ) * unitNormal + allParticles [ l ] . LinearVeloci ty ( ) ) ; // / / Прео бразуем скорости з ускорения . //

vector_3d accelerationl allParticles [ O ] . LinearVelocity ( ) / changeinTime ; vector 3d acceleration2 = allParticles [ l ] . LinearVelocity ( ) / changeinTime ; / / Находим силы , дейстзухщие н а объекты . allParticles С О } Force ( accelerationl * allParticles [ O ] . Мass ( ) ) ; al lParticles [ l ] . Force ( acceleration2 * allParticles [ l ] . Мass ( ) ) ; •

Задачу нахождения сил в столкновении можно решать по-разному. Ранее в этой главе мы использовали работу и законы сохранения импуль­ са и энергии, чтобы найти скорости объектов после столкновения. Если мы учтем , что каждый кадр соответствует определенному интервалу вре­ мени, мы можем применить полученные формулы, чтобы вычислить из­ менение скорости в течение этого интервала. А это и есть ускорение изменение скорости, деленное на изменение времени. Знание ускорения материальной точки позволяет нам применить формулу F = ma. Эта фор­ мула действительно пригодится нам при решении многих задач. Функция HandleColl i sion { ) вычисляет скорости материальных то­ че.к после стол.кновенин по выведенным нами ранее формулам . Вот эти формулы:

v� = v1 + (v� P - v1 P ) n v� = v2 + (v�P - v2P ) n Чтобы использовать эти формулы, нужно найти единичный вектор, направленный вдоль линии взаимодействия объектов при столкновении. Функция HandleCollision { ) находит этот вектор, вызывая метод vec-

Стол кно вения м ате р и ал ьных точек

21 5

tor_Зd : : Normalize { ) в строках 1 0- 1 1 . Вектор расстояния передается функции HandleCol l i s ion { ) в параметре separationD is tance из функции UpdateFrame { ) . Найдя единичный нормальный вектор, функция HandleCollision ( ) скалярно умножает на него векторы скоростей объектов. Это позволяет найти компоненты векторов скоростей в направлении взаимодействия тел при столкновении. Чтобы найти скорости объектов после столкновения, функция Hand­ leCollision { ) должна использовать коэффициент восстановления. Для обеспечения максимал:ьной универсальности класс dЗd_yoint_mas s по­ зволяет каждому объекту хранить свой коэффициент восстановления. Это позволяет делать некоторые объекты более упругими, чем другие. Однако при столкновении объектов используется только один коэффициент вос­ становления, поэтому функция HandleCollision ( ) находит средний ко­ эффициент восстановления пары объектов и использует его в расчетах. Это делается в строках 21-22.

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

Скорости объектов после столкновения вычисляются в строках 24-40 . Найдя эти скорости, функция HandleCollision { ) может найти ускоре­ ния тел вследствие столкновения. Это делается в строках 4 5-48. Из уско­ рений функция HandleCol l ision ( ) определяет силы, действующие на объекты, используя формулу F = ma.

РЕНД Е РИНГ КАДРОВ В главе 7 вы видели, как просто выполнять рендеринг кадров. Если мы правильно смоделировали всю физику и использовали эту физику для на­ хождения матрицы перемещения для Direct3D, то рендеринг сложности не представляет. В этой главе в функцию, выполняющую рендеринг, до­ бавлена только одна строка. Взгляните на листинг 8 . 5 . Листи нг 8 . 5 . Рендеринг столкновения шари ков 1 2

3 4 5 6

bool RenderFrame ( ) { all Particles [ O ] . Render ( ) ; allParticles [ l ] . Render ( ) ; return ( true) ;

21 6

Глава 8

Единственное, что нужно сделать функции RenderFrame { ) , - вы­ звать метод dЗd_J?oint_mass : : Render ( ) для двух объектов, а не для од­ ного. Ничего сложного.

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

Гла ва 9

Д и на м и к а т в е р д ых тел В компьютерных играх часто присутствуют объекты более сложной формы, чем рассматривавшиеся нами до сих пор сферы и треугольники. Вам, веро­ ятно, захочется моделировать автомобили, которые поворачиваются и, воз­ можно, переворачиваются, драконов, которые могут парить и пикировать, и персонажей, способных стрелять, прыгать и бросать предметы. Цель этой главы - создать общую физическую модель для работы со сложными твердотельными объектами такого рода.

Т вердые тела Золотое правило моделирования ( и вообще физических расчетов) упрощай. Нужно получать как можно больше от максимально упро­ щенной модели. Любой обычный объект чрезвычайно сложен. Он состоит из астроно­ мического количества атомов, взаимодействующих между собой по слож­ ным законам. Смоделировать взаимодействие всех атомов для объектов , которые мы можем видеть невооруженным глазом, невозможно. Поэтому мы поступим следующим образом. Вероятно, вы заметили, что большая часть объектов вокруг вас сохраняет свою форму с течением времени. Ваша микроволновая печь выглядит сегодня так же, как вчера, если только она не расплавилась от перегрузки. Все объекты, сохраняю­ щие свою форму, мы будем называть тверды.ми тела.ми (rigid bodies) и предположим, что эти объекты будут сохранять свою форму в течение всего времени их существования. Любой реальный объект не сохраняет свою форму абсолютно неизмен­ ной. Теннисный мяч сплющивается при ударе ракеткой, а потом восста­ навливает свою форму. Если вы ударите микроволновую печь кувалдой или расплавите ее, она (вероятно, необратимо) изменит свою форму. Но в играх многие объекты можно моделировать как твердые тела. Твердые объекты удобно представлять в виде наборов материальных то­ чек, как на рисунке 9 . 1 . Гантель с рисунка 9 . 1 состоит из двух соединенных между собой материальных точек. Мы пока проигнорируем массу перемыч­ ки. У каждой из этих материальных точек есть масса, и на нее могут дейст­ вовать силы, включая и воздействия других материальных точек.

Гл ава 9

21 8

Рис. 9 . 1 . Представление твердого объекта как набора материальных точек

В твердых телах расстояния между материальными точками являет­ ся постоянным . Это значит, что расположение любой материальной точ­ ки относительно других в пределах твердого тела будет неизменным. Другими словами, объект сохраняет свою форму. Как выясняется, такое представление весьма полезно на практике. Оно лежит в основе ряда концепций, сильно упрощающих моделирова­ ние объектов реального мира.

Ц е н тр м асс Брошенный мяч опишет в воздухе плавную кривую - параболу, как показа­ но на рисунке 9 . 2 . Траектория движения брошенного томагавка выглядит значительно сложнее, как показано на рисунке 9 . 3 . Но если присмотреть­ ся, станет ясно, что одна из точек томагавка движется по той же парабо­ ле. Эта точка называется центро.м м а с с ы (center of mass). Посмотрим, сможем ли мы найти ее местоположение с помощью математики.

/

'

/

/

/

/

/

./

/""' -- - - - -..... ......

'"

"

"



\

\

\

Рис. 9 . 2 . Мяч в полете движется no п арабол е

\

21 9

Дин а м и ка т ве рдых тел

Рис. 9 . 3 . У брошенного томагавка по параболе движется центр массы

Объекты состоят из множества маленьких частиц, каждая из кото­ рых обладает определенной массой и подвергается воздействию опреде­ ленных сил. Силу, действующую на i-ю частицу, можно представить в таком виде: F. = ml. a l. l

=

2 m .d x. dt2 1

Здесь mi - масса i-й частицы, а в следующем уравнении:

F =

xi

1

d2 m . x. dt2 l

l

- ее местоположение, как показано

L Fi = L d2mix/dt2 = d2 /dt2 L mixi i

i

i

Это просто другая форма записи уравнения F = ma. В данном случае мы представили его с помощью производных - ускорение есть вторая производная от перемещения по времени. Если определить центр масс как xcm в формуле:

где М - общая масса твердого тела, то общую силу, действующую на тело, можно выразить несложной формулой :

F

=

Md2 Xcm dt 2

220

Глава 9

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

Подска зка За счет совмещения центра массы тела и центра сетчатой модел и часто удается сильно упростить вычисления.

В р а щение д в умерных т вердых т ел Итак, мы разобрались с общим местоположением и скоростью твердого тела. Нужно просто рассматривать центр массы как материальную точ­ ку. Это просто. А как насчет вращательного движения? Вероятно, вы не удивитесь, узнав, что с ним все гораздо сложнее. Давай­ те сначала разберемся с вращением двумерных твердых тел на плоскости чтобы сделать это, нам не придется погружаться в замысловатую матема­ тику� Поскольку объекты могут двигаться только в одной плоскости, они могут вращаться только вокруг осей, перпендикулярных этой плоскости. На рисунке 9 . 4 показано твердое тело, ведущее себя таким образом.

Рис. 9.4. Твердое тело, вращающееся в плоскости вокруг оси z

Твердые тела, которые могут двигаться только в одной плоскости, встречаются весьма часто даже в ЗD-играх. Если вы толкаете объекты по полу, их движение можно моделировать 2D-механикой, если они не от­ рываются от поверхности. Шестеренки и колеса тоже можно представ­ лять в виде двумерных твердых тел, если они не опрокидываются. Математика, необходимая для моделирования двумерных твердых тел, куда проще математики, необходимой для моделирования трехмерных твердых тел, поэтому ее стоит использовать везде, где это возможно.

221

Дин а мика т в ердых тел

Для объектов , которые могут двигаться только в одной плоскости, вращение можно описывать одним скаляром - е, как показано на рисун­ ке 9 . 5 . Угол мы будем измерять в радианах.

положение

Ри с . 9 . 5 . Вращен ие двумерн ого твердого тела можно описы вать одним углом ()

По аналогии с одномерной кинематикой мы можем определить угло­ вую скорость ш и угловое ускорение а :

ш = d8/dt а = dш/dt = d2(:;1/dt2

Угловая скорость измеряется в радианах в секунду (рад/с), а угловое ускорение - в радианах в секунду за секунду (рад/с2).

Мате риальные точки в двумерном твердом теле В этом разделе мы изучим следующий вопрос: если у нас есть данные о твердом теле (его местоположение, скорость, ускорение, угловые ско­ рость и ускорение), то, как найти скорость и ускорение частицы в твер­ дом теле? Это важный вопрос, поскольку полезность модели твердого тела основана на возможности приложения внешних сил к разным ее ча­ стям одновременно. Рассмотрим локальную систему координат, движущуюся с той же скоростью, что и центр масс тела. Твердое тело поворачивается на угол е. Сосредоточим свое внимание на одной точке твердого тела, находящейся на расстоянии r от центра масс. При вращении тела она проходит путь по дуге длиной arcLength. Согласно определению радиана,

(:;1 = arcLength/r Если продифференцировать это выражение по времени, мы получим выражение для угловой скорости:

Глава 9

222 d61/dt = d/dt (arcLength/r) или

ш = ( l/r) d arcLength/dt Здесь darcLength/ dt есть угловая скорость, показанная на рисун­ ке 9.6. Для твердого тела угловая скорость есть общая скорость частицы в локальной системе координат, поэтому мы обозначим ее v. Це н тр массы

Угло вая ско р ость

Конечн ое п оложение

arcLength Начал ь ное п ол оже н ие Рис. 9 . 6 . Угловая скорость частицы в двумерном твердом теле

Можно выделить 1 / r из выражения производной, поскольку рассто­ яние от частицы до центра масс в твердом теле есть величина постоянная. Вообще все расстояния между частицами в твердом теле есть величины постоянные, и они не изменяются, когда мы берем производную.

v = rw Чтобы определить тангенциальное ускорение (tangential acceleration) частицы, нужно еще раз продифференцировать это выражение по времени:

dv/dt = rdw/dt

Обратите внимание - я сказал: � тангенциальное ускорение » . Есть и еще одно ускорение - центростремительное. Как показано на рисун­ ке 9. 7, тангенциальное ускорение изменяет величину вектора скорости частицы, а центростремительное ускорение изменяет его направление.

223

Дин а м и ка т в ердых тел

Пока мы в основном сосредоточимся на тангенциальном ускорении, но постепенно мы разберемся с обоими . Центр массы Тангенциальное ускорение

Рис. 9 . 7 . Тангенциальное и центростремительное ускорение

Центростремительному ускорению соответствует центростремитель ­ ная сила. Эта сила заставляет частицу отклоняться от прямолинейной траектории движения . Если бы вы сидели на вращающейся частице, вы бы чувствовали, что на вас действует сила, сбрасывающая вас с частицьt , действующая в направлении, обратном направлению центростремитель­ ной силы. Вы бы почувствовали эту силу, поскольку вы не прикреплены намертво к вращающейся частице, и ваше тело стремится двигаться no прямой . Сила, противодействующая центростремительной, называется центробежной (centrifugal force). Вот краткий вывод формулы для нахождения центростремительного ускорения с помощью конечных разностей. За незначительный интервал времени Л t центростремительное ускорение изменяет направление векто­ ра скорости частицы, но не его величину, как показано на рисунке 9 .8 . На рисунке изменение скорости из-за действия центростремительного ускорения обозначено Лv. Лv равно произведению центростремительного ускорения и Лt:

Изменение в направлении вектора тангенциальной скорости происхо­ дит при круговом движении частицы . Точнее говоря, оно происходит при движении частицы по дуге. Помните величину arcLength, использовав­ шуюся ранее в этой главе? Она как раз и обозначает дугу, пройденную ча­ стицей. Это значит, что мы можем записать :

ЛarcLength/r = Лv/v Здесь тангенциальная скорость определена как производная длинъ� дуги, поэтому с помощью конечных разностей мы можем записать :

ЛarcLength = vЛt

Гл ава 9

224

Начальн ая тангенциальная скорость

Конечная тангенциальная ско рость

Центростр емительное уско р ен ие Рис. 9.8. Изменение угловой скорости под действием

центростремительного ускорения

Чтобы найти центростремительное ускорение, выполним подстанов­ ки для ЛarcLength и Лv: ас =

Лv/Лt = vЛarcLength/rЛt = =

v2Лt/rЛt = v2 /r

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

В ра ща ющи й момент и момент инерции Ручки н а дверях размещаются как можно дальше о т петель п о опреде­ ленной причине. Попытайтесь закрыть дверь, толкая ее в точке вблизи петель. Вам это удастся (скорее всего), но ценой заметных усилий. Эту особенность твердых тел описывает вращающий момент (torque). Он обозначается т и определяется следующим образом: r =

r х F

Здесь r расстояние от центра масс твердого тела до точки, к которой мы прикладываем силу F. Если объект может двигаться только в одной плоскости, то момент всегда указывает в одном направлении, и мы мо­ жем найти его, обойдясь скалярами : -

т = rFsin(8)

Здесь () есть угол между векторами r и F. Если сила направлена по ка­ сательной к окружности (как сила трения о землю для катящегося коле­ са), можно даже записать:

225

Ди на м ика тв ердых тел

Теперь, определив концепцию, попробуем связать ее с той механи­ кой, которую мы уже рассматривали ранее. Рассмотрим вращающий мо­ мент i-й материальной точки в твердом теле. ri =

riFti

Тангенциальная сила, действующая на частицу, равна произведению массы этой частицы на ее тангенциальное ускорение (согласно второму закону Ньютона): Fti = miati Поэтому вращающий момент частицы можно записать в виде: ri =

rimiati

В предыдущем разделе мы выяснили, что в.� = ra, где а рение. Тогда r·1

-

угловое уско­

= r-1 2m 1·a

Чтобы получить общий вращающий момент твердого тела, нужно просуммировать вращающие моменты всех его частиц: Сумма в скобках - это момент инерции (moment of inertia), обычно обозначаемый 1 . т

=

� .L.

� 1 m1. = 1 = а .L. r-2

Т·

i

r

= Ia

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

ВЫЧИСЛЕНИЕ МОМЕНТА ИНЕРЦИИ Использование второго закона Ньютона для вращательного движения по­ зволяет вычислять момент инерции для любого твердого тела. Для мно­ гих форм твердых тел можно просто просуммировать моменты инерции составляющих их частиц по формуле : � r.1 2m1. 1 = .L.

Моменты инерции будут вычисляться тем точнее, чем больше частиц как можно меньшего размера мы примем во nнимание.

226

Глава 9

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

I =

J r2dm

На рисунке 9 . 9 перечислены некоторые широко распространенные формы тел и соответствующие им моменты инерции, найденные по этой формуле.

П устотелый цилиндр { или кольцо) относительно оси цили ндра

Однородный цилиндр

Однородный цилиндр (или диск)

относительно

1 = ( M R2)/2

\

(или диск) относительно центрального диаметра

оси (С)

l = ( MR2)/4+(M l 2)/1 2

Тон ки й стержень относительно ос и , проходящей через центр массы и перпендикулярной длин е

1 = ( М12)/ 1 2

Тонкий стержень относительно оси, проходящей через один из концов стержня и перп ендикуляр н о й длине

1 = (М12)/З

(е)

Однородная (заполненная) сфера относительно любого диаметра

1

=

(2MR2)/5

1 = ( MR2)/2

(f)

Тонкая сферическая оболочка относител ьно любого диаметра

(g)

Обруч относительно любого диаметра

(d)

1 Ось

(i)

Рис. 9.9. М ом е нт ы инерции для н е котор ых

=

(2MR2)/3

{h)

О бруч относительно любой касатель ной л инии

1 = (3MR2)/2

форм твердых тел

(j)

2 27

Ди на мика т ве рдых тел

Зная момент инерции твердого тела относительно какой-нибудь оси, можно найти его момент инерции относительно любой другой оси, парал­ лельной этой. Это свойство описывается теоремой Гюйгенса, известной также как теорема о параллельных осях (parallel axis theorem):

1 =

Icm

+ Mh2

Здесь Icm - момент инерции твердого тела относительно его центра массы, М - масса этого тела, h - расстояние до новой оси, как показано на рисунке 9 . 1 0 .

Ось вращения

Ось, проходя щая ч ерез центр массы

h

Рис. 9. 1 О. Теорема Гюйгенса

На рисунке 9 . 10 показана гантель, вращающаяся вокруг оси, не про­ ходящей через ее центр массы. Гантель - это твердое тело, состоящее из двух тяжелых элементов, которые можно рассматривать как материаль­ ные точки. На примере левой сферы рисунок демонстрирует, что матери­ альные точки могут вращаться вокруг оси, не проходящей через центр массы. Эта ось находится на расстоянии h от оси, проходящей через центр массы гантели . С помощью теоремы Гюйгенса можно найти момент инерции твердого тела, составленного из твердых тел более простой формы. Моменты инер­ ции суммируются, если они вычислены относительно одной и той же оси, поэтому нужно применять теорему Гюйгенса, чтобы преобразовать мо­ менты инерции отдельных частей тела относительно их осей в моменты инерции относительно общей оси вращения тела.

ПРИМЕР П РИМЕНЕНИЯ ТЕОРЕМЫ ГЮЙ ГЕН СА Попробуем найти момент инерции относительно центра масс для тела, изображенного на рисунке 9 . 1 1 . Это твердое тело состоит из трех элемен­ тов: двух однородных сфер и обруча, расположенных так, как показано на рисунке. Масса каждой сферы равна 200 кг, а радиус - 1 м. Радиус обру­ ча - тоже 1 м, а его масса - 100 кг. Все твердое тело может двигаться толь­ ко в одной плоскости, поэтому мы будем рассматривать его как двумерное.

Глава 9

228 Зам ечание

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

Р и с . 9. 1 1 . Твердое тело, состоящее из нескольких п ростых

форм

Первое, что нам нужно сделать, - найти центр масс. Вот определение центра масс:

Поместим начало системы координат в центр обруча. Общая масса М равна 200 кг + 200 кг + 100 кг = 500 кг. По рисунку 9 . 1 1 можно найти центр массы - его координаты Хеш (по оси х) и Уcm (по оси у) :

=

( 200

кг

• О м + 200

кг

• О м + 100

Уст = =

(200

кг

• (-2 м ) + 200

кг

( 1 /М)

кг

• О м ) / 500

кг =

Ом

L miYi i

• 2 м + 100

кг

• О м ) / 500

кг =

Ом

Результат, который мы получили, вероятно, был очевиден для вас: центр массы рассматриваемой фигуры расположен в центре обруча, и в выбранной нами системе координат его координаты равны (О ; О).

229

Дин а м и ка тверды х тел

Теперь с помощью теоремы Гюйгенса можно найти моменты инерции каждого элемента тела относительно оси, проходящей через центр массы тела. Расстояния от центра массы каждого элемента до центра массы тела приведены в таблице 9 . 1 . Таблица 9 . 1 . Расстояния от центра массы каждого элемента

до центра массы тела Элемент

Расстояни е

Нижняя сфера

2 м

Верхн яя сфера

2 м

Обруч

О м

Нам понадобятся моменты инерции каждого элемента относительно его центра массы. Момент инерции однородной сферы относительно лю­ бого диаметра равен (2/5)MR 2 , поэтому момент инерции каждой сферы равен 80 кг • м 2 . Момент инерции обруча относительно центральной оси равен MR2 , поэтому в нашем примере момент инерции обруча равен 100 кг • м 2 . Это вся нужная нам информация. Просто применим теорему Гюйген­ са. Для двух сфер результаты будут одинаковыми:

1sphe re = 1ст +

Mh 2 = 80 кг• м2

+

200 кг ( 2 м ) 2

=

880 кг • м2

Mh 2 = 100 кг• м2 + 100 кг ( О м ) 2

=

100 кг • м2

Для обруча:

Iь оор =

1cm +

Общий момент инерции тела равен сумме моментов инерции его эле­ ментов:

1 =

2 Isphere + lhoop = 2 ( 880 кг·м2)

+

100 кг·м2

=

1 860 кг · м2

Т в ердые тел а в З D Разобравшись в поведении двумерных твердых тел, можно начать пере­ ход к трехмерным. Хотя можно создать множество игр, обходясь только двумерными твердыми телами, все больше и больше игр используют трехмерные. Например , нельзя создать авиасимулятор без трехмерных твердых тел. Математика, используемая для описания поведения трехмерных твер­ дых тел, довольно замысловата, и мы будем разбираться в ней шаг за ша­ гом. Начнем с изучения вращения трехмерного твердого тела вокруг

230

Глава 9

произвольной оси. В любой момент времени это твердое тело будет вращать­ ся вокруг выбранной оси с определенной угловой скоростью, как показано на рисунке 9 . 1 2 . Это вращение можно описать с помощью направленного вдоль оси вращения тела вектора угловой скорости ш , длина которого рав­ на угловой скорости.



Рис . 9. 1 2 . Вектор угл овой скорости описывает угловую скорость

вращения вокруг произвольной оси

Зам ечание Почему мы начали с угл овой скорости, а не с вектора, определяющего ори­ ентаци ю, аналогично скаляру ориентации е в двумерном случае? Потому что связь между вектором ориентации и угловой скоростью в этом случае не такая простая , как в двумерно м . Собственно говоря, ш, вектор угловой скорости, не является производн ы м какого-то другого вектора. Почти все параметры (угловая скорость, угловое ускорение и вращающий м омент) определяются похожими м етодами, поэтому мы начнем с н их.

Вектор углового ускорения времени :

а

есть производная угловой скорости по

а =

dw /dt

Вроде бы неплохо. Эта формула выглядит привычно. Мы просто при­ менили в ней векторы. Следующее, что мы сделали для двумерного тела, - связали угловые скорость и ускорение со скоростями и ускорени­ ями частиц, образующих твердое тело. Попробуем сделать то же самое и для трехмерного тела. Скорость v частицы, расположенной в точке r относительно центра массы тела, будет определяться соотношением:

v = ro X r

231

Дин а м и ка т в е рдых тел

Вращение вокруг произвольной оси можно свести к двумерному слу­ чаю, спроецировав вектор r на плоскость, перпендикулярную оси враще­ угол между двумя ния. Длина вектора будет равна r sin8, где е векторами . В предыдущем разделе мы вывели формулу для скорости в двумерном случае: v = шr. Эта формула соответствует длине вектора, по­ лучаемого в результате векторного произведения. -

l a х b l = аЬ sin(8) l vl

= lw

х rl

lv/ =шr sin(8) Вектор скорости по определению должен быть перпендикулярным и радиусу, и оси ориентации. Теперь можно найти ускорение материальной точки в твердом теле, взяв производную по времени от вектора скорости:

а = dv/dt = d(w x r)/dt = = (dш/dt) x r + ш x (dr/dt) = a x r+w x (w x r) Итак, первый элемент в полученном результате - тангенциальное ускорение, а второй - центростремительное. Попробуйте сравнить этот результат с выражениями для двумерного случая: at

=а х r

ас = w х (w х r) За м еч а н и е Все изложенное может показаться малопонятным, если вы не знакомы с векторным анализом. Возможно, оно останется малопонятным, даже если вы с н и м знакомы . Я признаю, что недостаточ н о храбр, чтобы пытаться из­ лагать в этой книге основы векторного анализа, но, если вы хотите с ним по­ знакомиться, попробуйте, например, книгу Н. М. Schey «Div, grad, curl and а /1 that». Если вас интересует более п одробное рассмотрение тех п роблем, ко­ торые мы разбираем здесь, п оп ро буйт е почитать книги по теоретической механике, например, Herbert Goldsteiп «Classical Mechanics». 1

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

aworld

1

=

acm

= Vcm + W

+w

Х

Х

r+w

r

Х

Отечественному читателю доступнее Смирнов В. И. и Голубева О. В.

«Теоретич,еская механика» .

(w

Х

r)

«Курс высшей математики»

Обе книги переиздавались много раз

и выложены в Интернете, например, на сайте lib. mexmat.ru.

-

(Прим. перев.).

232

Глава 9

В ра щающий момент в ЗD Определение вращающего момента для 3D у нас уже есть - оно такое же, как и для 2D:

't = r X F

Это, конечно, неплохо - по сравнению с предыдущим разделом. Но не спешите радоваться - посмотрите, что будет дальше. Мы применим тот же прием, что и раньше - попытаемся найти вра­ щающий момент i-й частицы твердого тела. Сначала мы подставим выра­ жение для прикладываемой силы F:

Fi = miati ri = ri х miati = miri х ati

Тогда

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

ati = а х ri ri = ri х miati = miri х а х ri

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

r = Li ri = 2:miri xax ri Для двумерного случая мы просто выделили из выражения угловое ускорение и получили скалярную величину, которую назвали моментом инерции. Но в трехмерном случае все не так просто! Как выделить вектор а из нашего выражения? Посмотрим на компоненты векторов. Компоненты вектора ri обозна­ чим (х, у, z). Индексов i в выражении нет, чтобы оно было более компакт­ ным, но не забудьте, что мы суммируем вращающие моменты всех частиц твердого тела. Компоненты вектора а обозначим (ах, ау, az) · При­ готовьтесь - выражение будет то еще!

r = 2: mi {[+(y2+z2 )ax-хуау-xzaz ] х+ i

+[ -хуах +(z 2 +x2 )ay-yzaz] у+ +[ -xzax-yzay +(x2 +y2 )az] z}

233

Ди на м ика т ве рдых тел П од ска зка

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

Вероятно, выражение покажется вам непонятным и ни на что не по­ хожим, но сделаем несколько замен и посмотрим , что получится. Вот эти замены : 2 z. 2 I � � ( y.1 + 1 )m1· хх =

Iуу Izz

=

� ( z.12 +x.1 2 )m1· � i

� ( x·1 2 + y.12 )m1· = �

Тогда выражение для вращающего момента примет вид !

= (+

Ixxax - Ixf1y - Ixzaz)

(-

Iyxax

(-

Izxax - Izf1y

+

Х +

Iyf1y - Iyzaz ) у + +

Izzaz)

Z

Возможно, это выражение кому-то покажется знакомым? Это выра­ жение умножения матрицы на вектор в компонентной форме. Говоря другими словами, если мы выделим матрицу 1, компоненты которой бу­ дут равны множителям при компонентах вектора, мы получим :

l 't

xx

= -1 ух -1zx

-l

xy

lyy -1

zy

-

l xz

-1

yz

l zz

а

х

) cos(q>)

о

о

1

- sin(a)

о

о

cos(a)

о

sin(a)

о

о о

Rz =

о

о

1 о

о

cos(a) о

cos(0) sin(0) - sin(0) cos(0)

о

о о 1

о

о

о

о

о

о

1

о

о

о

о

1

237

Дин а м и ка т в е рдых тел

Теперь нам нужно определить, как эти матрицы будут изменяться с течением времени. Для начала сделаем каждый из углов поворота зави­ симым от времени. Для малых промежутков времени зависимости будут линейными:

lfJ = шхt р = ш уt 8 = Шzt

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

Теперь продифференцируем R по времени. Вы обнаружите, что

dR/dt = шR где

ш

есть матрица вида

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

RTR = I Такой подход хорош для обработки поворотов в играх. В нем просто разобраться, и он использует все те же операции умножения матриц, ко­ торые мы использовали все время. Эти операции можно оптимизировать, чтобы повысить скорость. Это неплохой выбор.

238

Глава 9

Подска зка Как уже говорилось в ы ш е , кватернионы в последнее в ремя стали очень популяр н ы м способом п редставления ориентации объектов в ЗD. Я на­

стоятельно рекомендую вам после прочтения этой книги изучить кватер­

нионы. Хотя связанная с н и м и математика довольно замысловата, они могут быть очень полезны .

Р еа л и з а ц ия твердых тел в З D Наконец, мы добрались до этапа создания класса, который будет пред­ ставлять твердые тела в 3D. После всего, что мы уже сделали, это будет несложно.

Замечание Код при мера программ ы и з этой главы есть на ком пакт-диске в папке so­ urce \Chapter0 9\RigidВody.

Кл асс dЗd_rig id_body Для моделирования твердого тела нужны переменные для хранения, на­ пример, массы, местоположения , скорости и суммарной действующей на него силы. Нам также понадобятся переменные для хранения вращатель­ ных величин . Кроме того, с твердым телом связана сетчатая модель. Центр массы это­ го тела считается началом системы координат для модели. Пока такое поло­ жение нас устраивает, но в играх от него, скорее всего, придется отказаться. В сложных твердых телах центры масс почти никогда не совпадают с нача­ лами систем координат сетчатых моделей, поэтому в последующих главах мы откажемся от такого совмещения. Для обнаружения столкновений в классе используется метод ограни­ чивающей сферы, поэтому в определении есть элемент для хранения ра­ диуса этой сферы.

Зам ечание Определение класса твердого тела содержится в файле PМRigidВody . h в папке Source \ ChapterO 9 \RigidВody.

В листинге 9 . 1 приведено определение класса твердого тела rigid_body.

-

dЗd

Дин а мика т вердых тел

239

Листинг 9 . 1 . Класс dЗd_rigid_body 1

class d3d_rigid_body

2

{

3

private : dЗd mesh objectмesh ;

4 5 6 7 8 9 10 11

// Физические сзойст:в а и характеристики линейного дв ижения .

scalar mas s ; vector_3d centerOfМassLocation ; vector_3d l inearVelocity ; vector_3d l inearAcceleration ; force sumForces ;

12 13 14

angle_set_3d currentOrientation ;

15 16

vector 3d angularAcceleration ;

// Характерис '1'Ики :вращательного дв ижения

vector_3d angularVelocity ;

vector 3d rotationalinertia ; vector 3d torque ; 18 19 D3DXМATRIX worldМatrix ; 20 21 2 2 puЫ i c : 23 d3d_rigid_body (void) ; 24 17

25 26

bool LoadМesh ( s td : : s tring meshFileName ) ;

27 28

void мas s (

29 30

scalar massValue ) ; s calar мass (void) ;

31 32 33

void Location ( vector_3d locationCenterOfМass) ;

34 35 36

vector_3d Location (void) ;

37

void LinearVelocity ( vector_3d newVelocity) ;

38

vector_3d LinearVelocity (void) ;

39 40

void LinearAcceleration (

41 42

vector_3d LinearAcceleration (void) ;

43 44 45 46

vector_3d newAcceleration) ;

void Force ( force sumExternalForces ) ; force Force (void) ;

240 47 48 49 50

Глава 9

void CurrentOrientation ( angle_set_3d newOrientation ) ; angle_se t_3d CurrentOrientation (void) ;

51 52 53 54

void AngularVelocity ( vector_3d newAngularVelocity) ; vector_3d AngularVelocity (void) ;

55 56 57

void AngularAcceleration (

58

vector_3d newAngularAcceleration) ; vector_3d AngularAcceleration (void) ;

59 60 61

void Rotationa1Inertia (vector_3d inertiaValue ) ; vector_Зd Rotationalinertia (void) ;

62 63

void Torque (vector_Зd torqueValue ) ;

64 65

vector_Зd Torque (void) ;

66

Ьооl Update (

67 68

scalar change inTime ) ; Ьооl Render (void) ;

69 } ;

Как и в классе dЗd_yoint_mas s , рассматривавшемся в предыдущих главах , в классе dЗd rigid body есть элемент-объект класса dЗd mesh. Он объявлен в строке4 лист;-нга 9. 1 . Кроме того, в строках 7- 1 1 объ;влены элементы, предназначенные для хранения параметров линейной динамики. Элементы данных, объявленные в строках 14- 18, хранят характеристики вращательной динамики твердого тела. В классе dЗd_rigid_body есть методы для чтения и записи значений элементов данных. Кроме того, есть методы Update ( ) и Render ( ) , о на­ значении которых вы, вероятно, уже догадались. Пока все просто и прямолинейно. Двинемся дальше - посмотрим, как этот класс используется.

И н и циализ ация объ екта класса dЗd_rigid_body В примере программы из этой главы демонстрируется использование класса dЗd_rigid_body. Вся логика, специфичная для этой программы, находится в файле RigidВodyTest . срр. В этом файле содержатся функ­ ции, требуемые платформой физического моделирования. Согласно требованиям платформы, инициализация Direct3D выпол­ няется в функции OnAppLoad ( ) . Единственный момент, достойный внимания - в программе используется версия этой функции, отключаю­ щая моделирование освещения Direct3D. Эта версия использовалась в первых примерах программ с треугольниками. В примерах с шариками

241

Дин а м и ка т в ердых тел

моделирование освещения не отключалось. Из-за способа использования материалов и текстур в данном примере моделировать освещение не нужно.

За м еч а н ие Чтобы увидеть этот пример в работе, скопируйте в рабочую директорию проекта файлы tiger . х и tiger . Ьmр, nоставляющиеся вместе с SDK Di­ rectX. Установив инструментарий SDK, nерейдите на диск, на который он установлен. На диске должна быть папка DXSDK. В ней есть подпапка Samp­ les \Мedia, в которой и находятся нужные файл ы .

Инициализация собственно объекта твердого тела выполняется в функ­ ции Gameinitialization ( ) . Код этой функции приведен в листинге 9 . 2 . Листинг 9 . 2 . Инициализа ция объекта твердого тела 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

Ьооl Gameinitialization ( ) { / / Создаем матрицу отображения - :ка:к в предыдущих примерах . D3DXVECTOR3 eyePoint ( O . O f , 3 . 0 f , - 1 0 . 0 f ) ; D3DXVECTOR3 lookatPoint ( O . O f , O . O f , 0 . 0 f) ; D3DXVECTOR3 upDirection ( O . O f , 1 . 0 f , O . O f) ; D3DXМATRIXA1 6 tempViewМa.trix ; DЗDXМatrixLookAtLН ( &tempViewМatrix , &eyePoint , & lookatPoint , &upDirection ) ; theApp . ViewМa.trix ( tempViewМa.trix) ; // Создаем матрицу проецирования D3DXМAТRIXA1 6 pro jectionМatrix ;

:ка:к

в предыдущих примерах .

D3DxмatrixPerspectiveFovLH ( &pro j e c tionМatrix , D3DX_PI / 4 , l . O f , 1 . 0 f , 1 0 0 . 0 f ) ; theApp . Proj e c tionМatrix {projectionМatrix) ; / / Загружаем сетчатую модель объе:к�а . theObject . LoadМesh ( " tiger . х" ) ;

20 21

theObject . AngularVelocity (vector_3d ( 0 . 0 , 0 . 0 , 0 . 0 ) ) ;

22

theObject . AngularAcceleration (vector 3d ( O . O , O . O , O . O ) ) ; _ the0bject . Rotationa1Inertia (vector_3d ( 3 9 . 6 f , 3 9 . 6f , 1 2 . 5f) ) ;

23 24 25 26 27 28 29 30 31 32

theObject . Torque (vector_3d ( 0 . 0 , 0 . 0 , 0 . 0 ) ) ;

1 1 При:кладываем силу , под воздействием :которой

/ / начнет двигаться объе:кт . force theForce ; theForce . Force (vector_3d ( l . 0 , 0 . 0 , 0 . 0f) ) ; theForce . ApplicationPoin t (vector_3d ( 0 . 0 , 0 . 0 , - 1 . 0 f) ) ; theObject . Force ( theForce ) ;

242

Глава 9

33 34

theObject . мas s ( l O O ) ;

35 36

return

( true) ;

В строках 4-9 листинга 9 . 2 функция Gameinitiali zation ( ) подго­ тавливает матрицу отображения. В строках 1 3 - 1 6 подготавливается матрица проецирования. Поскольку позиция наблюдения и перспекти­ ва не изменяются от кадра к кадру, их не нужно пересчитывать, как это обычно происходит в примерах программ, поставляемых с SDK DirectX. В нашем примере матрицы отображения и проецирования создаются то­ лько один раз и после этого не изменяются. В строке 19 листинга 9 . 2 функция Gameini tiali z ation ( ) загружа­ ет сетчатую модель, которая должна использоваться данным твердым те­ лом . Это сетчатая модель тигра из SDK DirectX. Инициализация объекта класса dЗd_rigid_body начинается в стро­ ке 2 1 . В строках 2 1 -24 задаются вращательные характеристики тигра. Вероятно, вас интересует, откуда взяты значения вращательной инерции в строке 23. Они вычислены - тигр рассматривался как цилиндр, и исполь­ зовались формулы из рисунка 9 . 9 . Обратите внимание, что в начальный мо­ мент работы программы тигр смотрит на вас - вдоль оси z . Если рассматривать тигра как цилиндр, то при вращении вокруг оси z исполь­ зуется формула I = MR2 / 2 . Но при вращении вокруг осей х или у испо­ льзуется формула I MR2 / 2 + ML2 / 1 2 . При расчетах я использовал длину 2 м (без учета хвоста) и массу 100 кг. Кроме того, в функции Gameinitialization ( ) также задается началь­ ная сила, приводящая тигра в движение. Это делается в строках 26-29. Здесь есть один важный момент. Я создал для этой программы класс force для представления сил. Вместо сил, представляемых векторами и действующих на объекты, в которых они хранятся, теперь силы пред­ ставляют собой самостоятельные объекты. Такое выделение необходимо, поскольку для описания силы во вращательной динамике нужно больше информации, чем в линейной. В линейной динамике силы всегда действу­ ют на центр массы тела. Во вращательной динамике это не так . Работая с вращающимися твердыми телами, нужно учитывать силы, приложенные к разным точкам поверхностей этих тел. Почему силы могут прикладываться к разным точкам твердых тел? Представьте себе автомобильный симулятор. Врезаться в другой авто­ мобиль можно с разных сторон - и не всегда с направления, параллельно­ го какой-либо оси координат. Поэтому нужно учитывать возможности приложения сил, действующих в разных направлениях и приложенных к разным точкам поверхностей тел. Все эти рассуждения - просто попытка объяснить, почему в объекте класса force хранятся как вектор силы, так и вектор, указывающий, куда эта сила прикладывается. Определение класса force приведено в листинге 9 . 3 . =

243

Дин а м и ка т в ердых тел Листинг 9.3 . Класс force 1 2 З 4 5 6 7

class force private : vector Зd forceVector ; vector Зd forceLocation ; puЬlic :

8 / / Ве :ктор собственио сипы . void Force ( vector_Зd theForce ) ;

9 10 11 12

vector_Зd Force (void) ;

13 / / Ве :ктор , у:казывающий точ:ку приложения силы . void ApplicationPoint (

14 15

vector_Зd forceApplicationPoint) ;

16 17 18 } ;

vector_Зd ApplicationPoint (void) ;

Этот класс прост. В нем содержатся всего два вектора и методы, по­ зволяющие читать и записывать значения этих векторов. Если вы верне­ тесь к листингу 9 . 1 , то увидите, что в классе dЗd_rigid_body есть элемент-объект класса force. Кроме того, там есть методы для чтения и записи значения этого элемента. В функции Gameini tializa tion ( ) , код которой приведен в листинге 9 . 2 , сила, действующая на тигра, зада­ ется вызовом метода Force ( ) в строке 29. Сила прикладывается не к центру массы тигра, поэтому она заставляет тигра не только двигаться поступательно, но и вращаться. Если вы запустите программу, то увиди­ те, что тигр медленно вращается, двигаясь по направлению к правому краю окна программы .

Обновление объектов кл асса dЗd_rigid_body Просчитывая каждый кадр, платформа вызывает функцию UpdateFra­ me ( ) , код которой приведен в листинге 9 . 4 . Листинг 9 . 4. Функция UpdateFrame( ) 1 2 З

bool UpdateFrame ( ) , { s tatic bool forceApplied

=

false ;

4 5 6 7

1 1 Если начальная сипа уже бЫJiа приложена . . . if ( forceApplied)

244

Гл ава 9

8

/ / Уменьшаем эту силу до О . force offCenterForce ; offCenterForce . Force (vector_3d ( 0 . 0 , 0 . 0 , 0 . 0f) ) ; offCenterForce . ApplicationPoint (vector_3d ( O . O , O . O , O . O ) ) ; theObject . Force ( offCenterForce ) ;

9 10 11

12 13

}

14 15

/ / В про'I'ИВном случае сила еще н е бЪ1Ла приложена . . . else

16 17 18

/ / Приложим ее . forceApplied=true ;

19

20 21 22 23

theObject . Update ( 1 ) ; return ( true) ;

Эта функция проверяет, была л и приJ1ожена к тигру начальная сила, заданная в функции Game initial i z ation ( ) . Во время просчета первого кадра анимации сила еще не была приложена, поэтому функ­ ция UpdateFrame ( ) прикладывает ее, вызывая метод dЗd_rigid _ body : : Update ( ) . После первого кадра сила уже была приложена. Мы не хотим, чтобы она прикладывалась снова, иначе тигр будет ускорять свое движение. Поэтому в строках 9-12 листинга 9 . 4 функция UpdateF­ rame ( ) уменьшает силу до О . Посмотрим на метод dЗd_rigid_body : : Update ( ) . Именно в нем проводятся все расчеты, связанные с моделированием физики. Листинг 9 . 5 . Метод dЗd_rigid_body: : Update ( ) 1

2 3 4 5 6 7 8 9

10 11

12 13 14

bool d3d_rigid_body : : Update ( scalar changeinTime } // / / Начинаем с расчета линейной .цинами:ки . // // Находим линейное ускорение . // а = F/m assert (mass ! =O ) ; linearAcceleration = sumForces . Force ( ) /mass ; // Находим линейную схорость . linearVelocity + = linearAcceleration * changeinTime ;

15 1б 17 18

/ / Находим новое местоположение центра иассы . centerOfМassLocation += linearVelocity * changeinTime ;

245

Ди на мика т вердых тел 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 */

// / / линейная динамика просчитана . //

/ / Создаем матрицу перемещения . D3DXМATRIX totalTranslation ; D 3DxмatrixTranslation ( &totalTranslation , centerOfМas sLocation . X ( ) , centerOfМassLocation . Y ( ) , centerOfМassLocation . Z ( ) ) ; // / / Начинаем расчет вращательной динамики .

11

/ / По известной силе находим вращающий момент .

torque =

sumForces . Appl icationPoint ( ) . Cross ( sumForces . Force ( ) ) ;

/ * П о вращающему моменту и инерции вычисляем УI"Ловое ускорение . */ angularAcceleration . X ( torque . X ( ) /rotationalinertia . X ( ) ) ; angularAcceleration . Y ( torque . Y ( ) /rotational inertia . Y ( ) ) ; angularAcceleration . Z ( torque . Z ( ) /rotationalinertia . Z ( ) ) ;

/ * Изиеияем УI"Ловую скорость согласно УI"Ловоиу ускоре НИJD .

49 50

angularVelocity + = angularAcceleration * changeinTime ;

51 52 53

// / / Используем УI"ЛОвое ускорение , что бы найти YI"J'Ibl вращения . //

54 55

currentOrientation . XAngle ( currentOrientation . XAngle ( ) + angularVelocity . X ( ) * changeinT:ime) ; currentOrientation . YAngle (

56 57 58 59 60 61 62 63 64 65 66

currentOrientation . YAngle ( )

+

angularVelocity . Y ( ) * changeinTime ) ; currentOrientation . ZAngle ( currentOrientation . ZAngle ( ) + angularVelocity . Z ( ) * change inT:ime) ; // // ЗаверШИJIИ расчет вращательной динамики . //

Глава 9

246 67 68 69 70 71 72

/ / Создаем матрицы вращения для каж;цой оси . D 3DXМATRIX rotationX , rotationY , rotationZ ; D3DXМatrixRotationX ( &rotationX , currentOrientation . XAngle ( ) ) ; D3DXМatrixRotationY ( &rotationY , currentOrientation . YAngle ( ) ) ; D3DXМatrixRotationZ ( &rotationZ , currentOrientation . ZAngle ( ) ) ;

73 74 75 76 77 78 89 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94

D3DXМATRIX totalRotations ; / / Перемножаем их , чтобы получить rлобальную матрицу . DЗDXМatrixМul tiply ( &totalRotations , &rotationX , &rotationY) ; D3DXМatrixМul tiply ( & totalRotations , &totalRotations , &rotationZ) ; / * Объединяем матрицы вращения и перемещения в rлобальиую матрицу . * / D3DXМatrixМultiply ( &worldМatrix , &totalRotations , &totalTransl ation) ; return ( true ) ;

Как видите, львиную долю расчетов выполняет именно этот метод. Первое, что он делает - находит линейное перемещение объекта по при­ ложенной к нему силе. Из приложенной силы и массы объекта метод Update ( ) находит ускорение. В строке 14 листинга 9 . 5 метод находит изменение скорости. По этому изменению скорости находится вектор смещения для центра массы тела. Этот вектор включается в матрицу пе­ ремещения в строках 24- 2 9 . Пока твердое тело воспринималось как материальная точка. Начиная со строки 36, метод Update ( ) начинает использовать отличия между ма­ териальными точками и твердыми телами. В строках 36-37 по формуле т = r х F вычисляется вращающий момент объекта. По вращающему моменту и инерции вращения метод находит линейное ускорение в строках 41-46. Затем по угловой скорости вычисляются углы вращения по осям х, у и z . Когда эти углы найдены, создаются матрицы вращения (строки 69-72). Работа метода Upda te ( ) почти закончена. Но прежде чем он завер­ шается, он совмещает все три матрицы вращения в одну в строках 7 7-84. Затем он объединяет матрицу вращения и матрицу перемещения в глобальную матрицу в строках 8 8- 9 1 . На этом выполнение метода Update ( ) заканчивается.

Дин а м и ка тв ердых тел

247

Рендерин г объекта класса dЗd_rigid_body Как и обновление объекта класса dЗd_rigid_body, рендеринг выполня­ ется двумя функциями. Первая из этих функций - RenderFrame ( ) , вы­ зываемая платформой. Код этой функции приведен в листинге 9 . 6 . Листинг 9 . 6 . Функция RenderFrame( ) 1

bool RenderFrame ( )

2

{

3 4 5 6

7 8 9

/ / Задаем матрицу отображения . theApp . DЗDRenderingDevice ( ) ->SetTransform ( DЗDTS_VIEW , &theApp . ViewМa.trix ( ) ) ;

/ / Задаем матрицу проецирования theApp . DЗDRenderingDevice ( ) ->SetTrans form (

10

DЗDTS_PROJECTION ,

11

&theApp . ProjectionМatrix ( ) ) ;

12 13

/ / Выполняем рендерииr объекта .

14

theObjec t . Render ( ) ;

15

return

( true ) ;

16

Как видно из листинга 9 . 6 , функция RenderFrame ( ) заново задает матрицы отображения и проецирования при рендеринге каждого кадра. Вы вправе спросить, необходимо ли это. Учитывая, что позиция наблюдения и видимая область остаются не­ изменными, ответ - нет. Для этого примера можно перенести вызовы функций в строках 4-1 1 в функцию Gameini tiali zation ( ) . Тогда почему же они расположены здесь? Чтобы проиллюстрировать один момент. В большинстве игр матрицу отображения нужно обновлять при просчете каждого кадра, поскольку камера непрерывно движется в мире игры. Матрица проецирования тоже непрерывно изменяется. Если в вашей игре будет также, то обе матрицы нужно задавать заново при ренде­ ринге каждого нового кадра. Их можно задавать в функции UpdateFra­ me ( } , но это должно быть последним действием функции. Если программе нужно изменить их, это лучше делать здесь, в функции RenderFrame ( ) . Рендеринг тигра выполняет вызываемый функцией RenderFrame ( ) ме­ тод dЗd_rigid_body : : Render ( ) , код которого приведен в листинге 9. 7. Как и для материальных точек, метод Render ( ) для твердых тел го­ раздо проще, чем метод Update ( ) . Метод Render ( ) сохраняет ранее ис­ пользовавшу:Юся глобальную матрицу и задает свою. Затем выполняется рендеринг сетчатой модели в заданной позиции, а после этого восстанав­ ливается ранее сохраненная глобальная матрица.

248

Гл ава

9

Листинг 9. 7 . Рендерин г твердого тела 1 2 З 4 5

bool dЗd_rigid_body : : Render (void)

{

/ / Сохраняем ма!t'рИЦУ г.nобального преобразования . DЗDXМATRIX saveWorldМatrix ; theApp . DЗDRenderingDevice ( ) ->GetTrans form (

6

DЗDTS_WORLD ,

7 8 9 10 11 12 13 14 15 16 17

&saveWorldМatrix) ;

18 19 20 21 22

/ / Применяем эту матрицу к объекту . theApp . DЗDRenderingDevice ( ) ->Setтransform ( DЗDTS_WORLD , &worldМatrix) ; / / Въшо.nияеи рен;церинг объехта noc.ne преобразований . bool renderedOK=obj ectмesh . Render ( ) ; / / Восстаиав.nиваем матрицу г.nоба.nьиого преобразования . theApp . DЗDRenderingDevice ( ) ->SetTransform ( DЗDTS_WORLD , &saveWorldМatrix) ; return

( renderedOK ) ;

И то г и В этой главе было много и математики, и программирования. Те инструмен­ которые у нас есть теперь, позволят нам моделировать почти любой объ­ ект в играх. Практически все можно предс1'авить в виде материальной точки, твердого тела, набора материальных точек или набора твердых тел. Как вы вскоре увидите, напш возможности стали очень обширnыми . Но, прежде чем мы сможем использовать эти классы в играх, нужно разобраться со столкновениями твердых тел . Этому посвящена следую­ щая глава. ты,

Гл а ва 1 0

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

Об на ружение ст ол кно в ений Какие методики обнаружения столкновений лучше всего использовать, зависит от того, какую игру вы пишете. Для многих игр достаточно ис­ пользовать грубые приближенные методики. Э1'О особенно верно для ар­ кад. Однако если вы пишете сложные имитаторы, в которых, например, нужно моделировать идущего человека, то грубые приближения не по­ дойдут . Поэтому для написания 3D-игр нужно знать обширный набор различных методик - от самых простых до наиболее сложных.

Г рубые п рибл и жения Основное преимущество грубых приближенньJХ методов обнаружения столкновений в играх - быстрота работы таких методов. Кроме того , их сравнительно просто реализовывать в коде. Иэ этой категории методов наиболее распространены методы ограничиваl()щих сфер, цилиндров и блоков прямоугольной формы. В главе 8 рассматривались алгоритмы об­ наружения столкновений между материальны:м:и точками, основанные на этих методах . Если вы используете в своей игре такие ме'l'оды обнаружения столк­ новений, это, в общем, означает, что вы аппроtLocation ( )

- obj ect2- >Location ( ) ;

dis tance AЬsValue ( distanceVector . Norm ( ) ) obj ectl ->BoundingSphereRadius ( ) =

-

obj ect2 - >BoundingSphereRadius ( ) ; / / Если расстояние почти нулевое . . . i f (CloseToZero (di s tance ) ) / / Оrраничизаuцие сферы соприка сают ся . col lisionStatus COLLISION TOUCHING ;

18 19

=

20 21 22 23 24

}

1 1 Иначе , если оrраничиsаuцие сферы перекрыва!О'I'ся . . . .

else if (

(distance < О . О )

collisionStatus

25

=

COLLISION_OVERLAPPING ;

26 27 28 29

return

( colli sionStatus) ;

30 31 bool collision : : CalculateReactions (void) 32 33 34 35 36 37

/ * Находим сре.циий коэффициент восстг�новпеиия , явтпацийся мерой эластичности обrъектов , участsующих в столкновении . scalar averageElasti city ( obj ectl ->CoefficientOfRestitution ( ) + obj ect2 ->Coefficien t0fRestitution ( ) ) /2 ;

38 39

//

40

// Теперь вычисляем числитель .

41

// vector_3d relativeVeloci ty

42 43 44 45

*/

=

=

obj ectl ->AngularVelocity ( ) vector_3d nwnerator

- object2- >AngularVelocity ( ) ;

=

- 1 * relativeVelocity *

( averageElasticity+ l ) ;

27 1

Стол кно в е н ия т в ердых тел 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94

// / / вычисляем зиаиена!I'е.пъ . Э!I'О сложно , поэ!I'оиу разделим / / вычисления н а несколько С!I'адий .

11

/ * Сначала находим единичный нормальный ве:ктор . Это нормализованный вектор , направnенный из центра массъ� объекта 1 к центру массъ� объекта 2 . * / vector_3d unitNormal oЬjectl->Location ( ) -object2->Location ( ) ; uni tNormal = uni tNormal . Normalize ( SCALAR_TOLERANCE ) ; =

/ / Теперь находим точку приложения силы к объекту 2 . vector 3d forceLocation2 = unitNormal * obj ect2 ->BoundingSphereRadius ( ) ; vector_3d tempVector = forceLocation2 . Cross (unitNormal ) ; / / Делим на инерцию вращения . terrpVector . X ( terrpVector . X () /object2 ->Rotationalinertia ( ) . Х ( ) ) ; terrpVector . Y ( tempVector . Y ( ) /object2 ->Rotationalinertia ( ) . У ( ) ) ; terrpVector . Z ( terrpVector . Z ( ) /object2 ->Rotationalinertia ( ) . Z ( ) ) ; / / Вычисляем векторное произведение результата и / / век!I'ора r для объекта 2 . tempVector tempVector . Cross ( forceLocation2 ) ; =

/ / Вычисnяем скалярное произведение вектора на / / единичный нормальный вектор . scalar partl unitNormal . Dot ( tempVector ) ; =

/ / Находим точку приложения силы к объекту 2 . unitNormal * = - 1 ; vector_3d forceLocationl unitNormal * obj ectl ->BoundingSphereRadius ( ) ; =

tempVector = forceLocationl . Cross ( unitNormal ) ; / / Деnим н а инерцию вращения . tempVector . X ( telrpVector . X ( ) / objectl ->Rotationalinertia ( ) . Х ( ) ) ; terrpVector . Y ( terrpVector . Y ( ) /objectl->Rotationalinertia ( ) . У ( ) ) ; terrpVector . Z ( tempVector . Z ( ) /obj ectl->Rotationalinertia ( ) . Z ( ) ) ; // Вычисnяем векторное произведение результата и / / вектора r для объекта 1 . tempVector = tempVector . Cross ( forceLocation l ) ; / / Вычисляем скалярное произведение вектора на / / единичный нормальный вектор . scalar part2 unitNormal . Dot ( tempVector) ; =

272

Гл ава 1 0

95 96 97 98

scalar denominator = 1 /objectl ->Мass ( ) + l / obj ect2 ->Mass ( ) + part2 + partl ;

99 100

// / / находим сумму сил для твердого тела 1 .

101

/ / Сначала находим импульсную силу , действующую

102

/ / при стоnхновении . force impulseForce ;

103 104 105

impulseForce . Force ( numerator/denominator) ; impulseForce . ApplicationPoint ( forceLocationl ) ;

106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130

/ / Добавляем постоянные силы ( если о ни есть ) . vector_Зd existingForce oЬjectl->Force ( ) . Force ( ) ; / / вычисляем суммарную сипу и сохраняем е е в =

объекте 1 . // force totalForce ; totalForce . Force (existingForce + impulseForce . Force ( ) ) ; obj ectl - >Force ( totalForce ) ;

//

/* Теперь находим сумму сил дпя твердого теnа 2 , и сохраняем е е в объекте 2 . * / // / / Получаем силы , уже действующие н а теnо 2 . existingForce

=

object2 ->Force ( ) . Force ( ) ;

/ / Добавляем х ним импульсную силу , / / действующую в обратном направлении . totalForce . Force ( existingForce - impul seForce . Force ( ) ) ; / / Сохраняем резуnътат в объекте 2 . obj ect2- >Force ( tota1Force) ; return ( true) ;

В строке 1 6 метод CollisionOccured ( ) проверяет, близко ли полу­ ченное расстояние к О. Используемая для этого функция CloseToZero ( ) содержится в файле P:ММЗ.thFunctions . h . Из-за погрешностей операций с плавающей запятой лучше не проверять, равен ли результат О. Если расстояние достаточно близко к О, чтобы считать его О , то нужно реагиро­ вать на столкновение. Поэтому, если функция: CloseToZero ( ) возвраща­ ет true, метод CollisionOccured ( ) считает, что сферы соприкасаются.

Стол кно в е н ия т в ердых тел

273

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

Если расстояние не близко к О, в строке 23 проверяется, не перекры­ ваются ли ограничивающие сферы. При этом расстояние будет меньше О. Если это так, функция CollisionOccured ( ) присваивает переменной collisionStatus значение, указывающее на столкновение с перекрытием. Если ни одно из условий не выполнилось, то изначально сделанное предположение оказывается верным - столкновения нет. Метод CalculateReactions ( ) начинается со строки 31 листинга 10.4. Он начинается с вычисления коэффициента восстановления столкновения в строках 35-37. Этот коэффициент вычисляется как среднее арифмети­ ческое к оэффициентов восстановления тел, участвующих в столкнове­ нии. Использование такого подхода позволяет учесть эластичность (или отсутствие таковой) всех тел, участвующих в столкновении . Остальная часть функции вычисляет импульсную силу по формуле, учитывающей и линейные, и угловые компоненты. Эта формула еще раз приведена ниже:

1

+

Это сложная формула, и вычисление результата по ней в методе Cal­ culateReactions ( ) разбито на несколько этапов. Сначала в строках

42-45 вычисляется числитель. Найти знаменатель сложнее. Его нахожде­ ние разделено на несколько шагов и выполняется справа налево. Сначала вычитанием векторов местоположен ий центров масс и нор­ мализацией результата находится единичный нормальный вектор. В ре­ зультате получается величина n из формулы. В строках 58 -59 метод CalculateReactions ( ) находит вектор, ука­ зывающий на точку соприкосновения. Как уже говорилось раньше, мы будем рассматривать точечные соприкосновения между ограничивающи­ ми сферами. В результате расчета мы получим величину r2 из выражения в знаменателе формулы. В строке 61 вектор r2 умножается на n. В строках 64-66 результат ум­ ножения делится на компоненты вращательной инерции тела 2 . Резуль­ тат деления умножается на r2 . На этом заканчиваются расчеты первого справа члена знаменателя. Весь процесс нужно повторить для второго справа члена. Это делается в строках 77-94.

Глава 1 0

274

В строках 96-97 метод CalculateReactions ( ) завершает вычисление знаменателя:. С помощью этого знаменателя: он находит вектор импульсной силы, прикладываемый к телу 1 (строка 104). Задав точку, в которой дейст­ вует импульсная: сила, он добавляет импульсную силу ко всем другим силам, действующим на тело. Это могут быть сила тяжести, силы от предыдущего столкновения: и другие силы. В любом случае находитсл общая: действующая: на тело сила. Импульсную силу нельзя: считать единственной действующей на тело, если вы хотите, чтобы игра выглядела реалистичной. Метод CalculateReactions ( ) заканчивается: добавлением отрицатель­ ной импульсной силы к силам, действующим на второе тело. Поскольку импульсная: сила действует на него в противоположном направлении, она вычитается: из суммы других сил, а не прибавляется: к ней. Разобравшись, к ак выполняется: обнаружение и обработка столкнове­ ний, можно посмотреть, как это делает программа. А что? Нужно сделать что-то еще, чтобы моделировать столкновения:? Увы, да. Посмотрите на листинг 10.5. Листинг 1 0. 5 . Функции обработки столкновений

1

bool UpdateFrame ( )

2 3 4 5 6 7

{ s tatic Ьооl forceApplied = false ; int i ; scalar timeincrement



1;

10

DWORD currentTime = : : timeGetTime ( ) ; if ( ! TimeToUpdateFrame ( currentTime) ) return ( true ) ;

11 12 13

/ / Дпя хаж.цого объехта . . . for ( i=O ; iAngularVelocity ( ) -obj ect2 ->AngularVelocity ( ) ; vector 3d numerator = - 1 * relativeVelocity * ( averageElasticity+l ) ; // // Находим знаменатель . Это сложно , поэтому делается / / в несколько шаrов . // /* Сначана находим единичный нормальный вектор , направленный из центра массы объекта 1 к центру массы объекта 2 . * / vector_3d unitNormal=obj ectl->Location ( ) -oЬject2->Location ( ) ; unitNormal = unitNormal . Normalize ( SCALAR_TOLERANCE ) ; / / Теперь находим точку приложения сил к объекту 2 . vector 3 d forceLocation2 = unitNormal * obj ect2 - >BoundingSphereRadius ( ) ; vector_3d tempVector = forceLocation2 . Cross (unitNormal ) ; / / Делим на инерцию вращения . tempVector . X ( tempVector . X ( ) / obj ect2 ->Rotationa1Inertia ( ) . Х ( ) ) ; tempVector . Y ( tempVector . Y ( ) / obj ect2 ->Rotationa1Inertia ( ) . У ( ) ) ; tempVector . Z ( tempVector . Z ( ) / obj ect2 ->Rotationa1Inertia ( ) . Z ( ) ) ; / / Перемножаем ответ с вектором r для объекта 2 . tempVector = tempVector . Cross ( forceLocation2 ) ; / / Перемножаем с единичным нормальным вектором . scalar partl = unitNormal . Dot ( tempVector ) ;

Сила тяжести и метате л ьные сн а ряды 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 }

295

/ / Теперь находим точку приложения сил к объекту 2 . uni tNormal *= - 1 ; vector 3d forceLocationl = uni tNormal * obj ectl ->BoundingSphereRadius ( ) ; tempVector = forceLocationl . Cross (uni tNormal ) ; / / Делим на инерцию вращения . tempVector . X ( tempVector . X ( ) / obj ectl - >Rotationalinertia ( ) . Х ( ) ) ; tempVector . У ( tempVector . У ( ) / obj ectl - >Rotationalinertia ( ) . У ( ) ) ; tempVector . Z ( tempVector . Z ( ) / obj ect l - >Rotationalinertia ( ) . Z ( ) ) ; / / Перемножаем ответ с вектором r для объекта 2 . tempVector = tempVector . Cross ( forceLocationl ) ; // Перемножаем с единичным нормальным вектором . scalar part2 = unitNormal . Dot ( tempVector ) ; scalar denominator = l /objectl ->Мas s ( ) + 1 /object2 - >Мass ( ) + part2 + partl ; // / / Прикладываем импульсную силу к объекту 1 . // force impulseForce ; impulseForce . Force (nwnerator/denominator) ; impulseForce . ApplicationPoint ( forceLocation l ) ; obj ectl->ImpulseForce ( impulseForce) ; // / / Прикладываем импульсную силу в обратном направлении к / / объекту 2 . // impulseForce . Force ( - l * impulseForce . Force ( ) ) ; obj ect2- >ImpulseForce ( impulseForce ) ; return ( true) ;

В версии метода collision : : CalculateReactions ( ) , приведенной в листинге 1 1 . 3 , для вычисления реакций твердых тел на столкновение используются только импульсные силы. Импульсные силы прикладыва­ ются к обоим телам (строки 73-84).

296

Глава 1 1

Теперь платформа готова поддерживать силу тяжести. Однако преж­ де чем программа примера сможет отобразить снаряды, врезающиеся в поверхность земли, в программе должна присутствовать эта поверхность, в которую можно врезаться. Поэтому нужно создать объект типа ground. Затем этот объект и силу тяжести нужно добавить в программу примера. Код класса ground приведен в листинге 1 1 .4. Л истинг 1 1 .4 . Соде ржимое файла ground . h

1 2 3 4 5

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

#include " PМFramework . h " using namespace pmframework ; class ground private : vector_3d location ; mesh groundМesh ; puЬlic : ground ( ) ; void Location (vector_3d newLocation) ; vector_3d Location ( ) ; bool LoadМesh ( s td : : s tring meshFileName ) ; bool Render (void) ; };

inline ground : : ground ( ) {

}

inline void ground : : Loca tion (vector_3d newLocation) location = newLocation ;

inline vector_3d ground : : Location ( ) return ( location ) ;

inline bool ground : : LoadМesh ( s td : : string meshFileName) return (groundМesh . Load (meshFileName ) ) ;

Сила тяжести и метател ьные сн аряды

297

40 } 41 42 inline bool ground : : Render (void) 43

44

return ( groundМesh . Render ( ) ) ;

45

В листинге приведено все содержимое файла, поскольку он является частью программы примера, а не частью платформы физического моде­ лирования. Класс ground реализован с использованием платформы, поэ­ тому в файл ground . h включен файл PМFramework . h. Пространство pmframework задействуется в строке 3 . В классе ground объявляются элементы данных (строки 8-9 листинга ground . h) . Первый элемент данных отслеживает положение поверхно­ сти, позволяя размещать ее выше или ниже начала глобальной системы координат. В этой программе поверхность считается горизонтальной, поэтому используется только компонент у вектора !оса tion. Элемент groundМesh позволяет программе загружать сетчатую мо­ дель и растровый рисунок для текстурирования поверхности:. В се методы класса очень просты. Конструктор не делает вообще ниче­ го. Другие методы читают и записывают значения элементов данных. В листинге 1 1 . 5 содержатся функции программы, моделирующей силу тяжести. Листинг 1 1 . 5 . Файл Launcher. cpp

1 2 3

#include " PМFramework . h " #include " Ground . h"

4

using namespace pmframework ;

5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

#define MILLISECONDS_PER_FRAМE 33 #define TOTAL ВALLS 5 rigid_body allBalls [TOTAL_BALLS ] ; ground theGround ; bool TimeToUpdateFrame ( DWORD currentTime ) ; void HandleOverlapping ( scalar timeincrement , int objectl , i n t object2 , collis ion &theCollision) ; bool OnAppLoad ( ) {

window_init_params windowParams ;

298

Глава 1 1

23 windowParams . appWindowTitle = " Gravity Tes t " ; 24 windowParams . defaultx= l O O ; 25 windowParams . defaultY=l O O ; 26 windowParams . defaultHeight=4 0 0 ; 27 windowParams . defaultWidth=4 0 0 ; 28 29 d3d_init_;params d3dParams ; 30 d3dParams . renderingDeviceClearFlags = D3DCLEAR_TARGET 1 31 D3DCLEAR_ZBUFFER ; 32 d3dParams . surfaceBackgroundColor D3DCOLOR_XRGB ( 0 , 0 , 2 5 5 ) ; 33 dЗdParams . enaЫeAutoDepthStencil = true ; 34 d3dParams . autoDepthStencilFormat = D3DFМТ_D 1 6 ; 35 d3dParams . enaЬleD3DLighting = false ; 36 theApp . InitApp (windowParams , d3dParams ) ; 37 38 return ( true ) ; 39 40 41 4 2 bool PreD3Dini tialization ( ) 43 { return ( true) ; 44 45 46 4 7 bool PostD3D initialization ( ) 48 { return ( true ) ; 49 50 51 52 bool Gameinitialization ( ) 53 { / / Создаем матрицу отображения - как в предыдущих примерах . 54 D3DXVECTOR3 eyePoint ( O . Of , 3 . 0f , - 1 0 . 0 f ) ; 55 56 D3DXVECTOR3 lookatPoint ( O . O f , 0 . 0f , O . Of ) ; D3DXVECTOR3 upDirection ( O . O f , 1 . 0 f , O . O f ) ; 57 58 D3DXМATRIXA1 6 tempViewМatrix ; D3DxмatrixLookAtLH ( 59 60 &tempViewМatrix , &eyePoint , &lookatPoin t , &upDirection ) ; 61 theApp . ViewМatrix ( tempViewМatrix) ; 62 63 / / Создаем матрицу проецирования . 64 D3DXМATRIXA1 6 pro j ectionМatrix ; 65 D3DxмatrixPerspectiveFovLH ( 66 &proj ectionМatrix , D3DX_PI/4 , 1 . 0f , 1 . 0f , 10 0 . 0 f ) ; 67 theApp . ProjectionМatrix (proj ectionМatrix) ; 68 69 vector_3d tempVector ( 0 . 0 , 0 . 0 , 0 . 0 ) ; force theForce ; 7О 71

299

Сила тяжести и метател ьные сн аряды 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121

/ * Загружаем сетчатую модель " шарика " . Не забудьте : эта модель поставляется вместе с SDK D irectx . Ее нужно скопировать из папки \Samples \C++\Direct3D\Tutorials \Tut0 6_Meshes , где - полный путь к папке , в которую установлен SDK D irectx . Скопируйте фаЙJIЪI tiger . x и tiger . bmp в папку проекта . * / al lBalls [ О ] . LoadМesh ( " tiger . х " ) ; / / Задаем начальное местоположение первого шарика . tempVector . SetxYZ ( - 3 . 0 f , 5 . 0 , 5 . 0 ) ; allBal l s [ O ] . Location ( tempVector) ; theForce . Force (vector_3d ( 0 . 0 , - 9 . 8 f , O . O ) ) ; theForce . ApplicationPoint ( tempVector) ; al lBalls [ O ] . ConstantForce ( theForce) ; / / Задаем вращательную инерцию первого шарика . tempVector . SetxYZ ( 3 9 . 6f , 3 9 . 6f , 1 2 . 5f) ; allBall s [ O ] . Rotationalinertia ( tempVector) ; // Задаем вектор си.пы, определяк:щий пикейное движение шарика . tempVector . SetxYZ ( l . 0 , - 1 . 0 , 0 . 0f ) ; theForce . Force ( tempVector ) ; // Задаем точку , к которой приложена сила . tempVector . SetxYZ ( 0 . 0 , 0 . 0 , - 1 . 0f ) ; theForce . ApplicationPoint ( tempVector ) ; // Сохраняем силу в объекте класса rigid_body . allBalls [ O ] . ImpulseForce ( theForce ) ; / / Задаем массу шарика . allBall s [ O ] . Мass ( l O O ) ; / / Задаем ограничива�ацую сферу шарика . allBall s [ O ] . BoundingSphereRadius ( 0 . 7 5 f ) ; / / Задаем упругость шарика . allBalls [ O ] . CoefficientOfRestitution ( 0 . 5 f ) ; / / Копируем характеристики первого шарика в о все остальНЪiе . al1Balls [ 4 ] al1Ball s [ 3 ] = al1Balls [ 2 ] = allBalls [ l ] allBalls [ O ] ; =

/ * Задаем другое начальное местоположение для второго шарик�. * / tempVector . SetxYZ ( 0 . 0 , 3 . 0f , 5 . 0 ) ; allBalls [ l ] . Location ( tempVector) ;

=

Глава 1 1

300 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138

139

140 141 142 143 144 145 146 147 148 149 150 151 152 1 53 1 54 155 156 157 158 159 160 161 162 163 1 64 1 65 166 1 67 168 169 170

theForce . Force ( vector 3d ( 0 . 0 , - 9 . 8 f , 0 . 0 ) ) ; theForce . ApplicationP�int ( tempVector) ; allBalls [ l ] . ConstantForce ( theForce ) ; / / ПриХJiа дываем другую силу . tempVector . SetxYZ ( - 1 . 0f , - 1 . 0f , O . O ) ; theForce . Force ( tempVector ) ; tempVector . SetxYZ ( 0 . 0 , - 1 . 0f , - 1 . 0 f ) ; theForce . Appl icationPoint ( tempVector) ; al lBall s [ l ] . ImpulseForce ( theForce ) ; / / Задаем упругость шарика . allBalls [ l ] . CoefficientOfRestitutiO n ( O . O O l f ) ; /* Задаем другое начальное местоположение для третьего шарика . * / tempVector . SetxYZ ( 4 . 0 , 4 . 0 f , 7 . 0 ) ;

allBalls { 2 J . Location { tempVector) ;

theForce . Force ( vector_3d ( 0 . 0 , - 9 . 8 f , 0 . 0 ) ) ; theForce . ApplicationPoint ( tempVec tor) ; al1Balls [ 2 ] . ConstantForce ( theForce ) ; / / ПриХJiадыв аем другую силу . tempVector . SetxYZ ( - 3 . 0 , 2 0 . 0 , 0 . 0 ) ; theForce . Force ( tempVector ) ; tempVector . SetxYZ ( l . O f , - 1 . 0 f , 0 . 0 ) ; theForce . ApplicationPoint ( tempVec tor) ; allBalls [ 2 ] . ImpulseForce ( theForce) ; / / Задаем упругость шарика . allBalls [ 2 ] . CoefficientOfRestitutiOn ( 0 . 17 f ) ; / / Задаем начальное местоположение · tempVector . SetxYZ ( 0 . 0 , 4 . 0 f , - 1 5 . 0 f ) ; allBalls [ 3 ] . Location ( tempVector ) ; / / Прикладываем силу тяжести . theForce . Force (vector 3d ( 0 . 0 , - 9 . 8f , 0 . 0 ) ) ; theForce . ApplicationP�int ( tempVector) ; all8alls [ 3 ] . ConstantForce ( theForce ) ; // Прикладываем импульсную силу . tempVector . SetxYZ ( 0 . 0 , 30 . 0 , 50 . 0 ) ; theForce . Force ( tempVector ) ; tempVector . SetxYZ ( 0 . 0 , - 1 . 0 f , O . O ) ; theForce . ApplicationPoint ( tempVector) ; allBalls [ 3 ] . ImpulseForce ( theForce) ;

Сила тя жести и метател ьн ые сн аряды 171 172 173 174 175 176 177 178 179 180 181 182 183 1 84 185 186 187 188 189 190 191 192 193 194 195 196 1 97 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 21 6

/ / Задаем упругость шарика . allBalls [ 3 ] . CoefficientOfRestitution ( 0 . 3f) ; / / Задаем начальное местоположение . tempVector . SetxYZ ( - 1 0 . 0f , 4 . 0 f , 5 . 0 f) ; al1Bal l s [ 4 ] . Location ( tempvector) ; / / Прикладываем силу тяжести . theForce . Force (vector_3d ( 0 . 0 , - 9 . 8 f , 0 . 0 ) ) ; theForce . ApplicationPoint ( tempVector) ; al1Balls [ 4 ] . ConstantForce ( theForce) ; / / Прикладываем импульсную силу . tempVector . SetxYZ ( l 0 . 0 , 50 . 0 , 0 . 0 ) ; theForce . Force ( tempVector ) ; tempVector . SetxYZ ( 0 . 0 , 0 . 0f , l . O ) ; theForce . ApplicationPoint ( tempVector) ; al1Ball s [ 4 ] . ImpulseForce ( theForce) ; / / Задаем упругость шарика . al1Balls [ 4 ] . CoefficientOfRestitution ( 0 . 6f) ; theGround . LoadМesh ( " seafloor . x" ) ; return ( true ) ;

bool HandleМessage ( НWND hWnd , UINT msg , WPARAМ wParam, LPARAМ lParam) return ( false) ;

bool Upd�teFrame ( ) int i ; scalar timeincrement = 1 ; DWORD currentTime = : : timeGetTime ( ) ; if ( ! TimeToUpdateFrame ( currentTime ) ) return ( true ) ;

30 1

Глава 1 1

302 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 2 52 253 2 54 2 55 256 2 57 258 259 2 60 2 61 2 62 2 63 2 64 2 65

// Для каJ!(дого объекта . . . for ( i=O ; i) должна быть намного жестче, чем пружины в нижнем конце, чтобы поведение «хвостика» было реалистичным. При движении го­ лова персонажа будет прикладывать силу к материальной точке в верхнем конце « хвостика>) . Эта материальная: точка передаст усилия: на первую пру­ жину. Пружина передаст усилие на следующую материальную точку и при­ ведет ее в движение. Эта последовательность движений будет передаваться: дальше по цепочке масс и пружин. Использование такого подхода позволит отслеживать положение каждого сегмента « хвостика>) . Когда нужно отобразить « хвостик» на экране, игра будет отображать его сегмент за сегментом. Внешнее представление каждого сегмента - это про­ сто сетчатая: модель, по форме похожая: на сосиску и обтянутая: текстурой. Программа должна совмещать расположение каждой модели с расположе­ нием соответствующей материальной точки. По мере движения: материаль­ ных точек будут двигаться: и модели. Реалистичность их движения: будет зависеть от масс материальных точек и характеристик пружин. Если у вас есть достаточная: вычислительная: мощность, можно моде­ лировать таким образом поведение каждой пряди волос в прическе.

31 2

Глава 1 2

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

Тка н ь Моделирование ткани похоже н а моделирование набора взаимосвязан­ ных « хвостиков » . Это иллюстрирует рисунок 1 2. 2. Внутреннее п редста вле н ие

Вне ш нее п редста влен и е

Рис. 1 2 . 2 . Внутреннее и внешнее п редставление ткани

Как видно из рисунк а 1 2 . 2, в программе ткань представляется в виде сетки материальных точек . Каждая точка связана с соседними точками по горизонтали, вертикали и диагонали посредством пружин . Пружины придают поведению ткани реалистичность . Внешнее представление тка­ ни - это просто плоская сетчатая модель, обтянутая текстурой с обеих сторон. Положение каждого вертекса модели должно соответствовать по­ ложению одной из материальных точек сетки. Реализуя в игре ткань, нужно сделать ячейки сетки гораздо меньши­ ми, чем на рисунке 1 2. 2 . Пружины должны быть гораздо мягче, чем в « ХВОСТИКе» . Одно из основных преимуществ использования этого метода для моде­ лирования ткани - материальные точки, образующие ткань, могут взаи­ модействовать с любыми другими объектами в игре. Для моделирования этого взаимодействия можно использовать рассмотренные в предыдущих главах методы обнаружения столкновений и реагирования на них. Если хорошо реализовать эти методы, то ткань будет оборачиваться вокруг других объектов, развеваться на ветру и вообще вести себя так, как дол­ жна вести себя настоящая ткань.

31 3

Системы м а сс и пружи н

О снова : га рмон иче ск ие коле б ания Представьте себе массу, подвешенную на пружине к потолку, как на рисун­ ке 12.3. Предположим, что в начале масса не подвешена к пружине. Если затем подвесить массу и отпустить ее, то под действием силы тяжести масса двинется вниз, растягивая пружину. Пружина приложит к массе направ­ ленную вверх силу. Под действием этой силы масса двинется вверх. Тяну­ щая ее вверх сила начнет спадать, и сила тяжести опять потащит массу вниз. Такие колебания теоретически будут продолжаться вечно. В реально­ сти они постепенно угаснут из-за неидеальности пружины и других причин, но пока проигнорируем это и будем считать, что колебания продолжаются вечно. Это пример незатухающих гармонических колебаний.

Рис. 1 2 . 3 . Масса, подвешенная на пружине к потолку

Маятник, показанный на рисунке 12.4, это еще более простой пример гармонических колебаний. Изучение поведения маятника позволит нам по­ нять физические основы простого гармонического движения. Это понимание, в свою очередь, позволит нам разобраться, как моделировать пружины. Если качнуть маятник, то груз в его конце будет выведен из равновес­ ного состояния. Сила тяжести будет тянуть груз к начальному вертикаль­ ному положению. Эта сила выражается таким равенством: -

F

= -mg

sinE>

Глава 1 2

314

- m g cos е mg

Р и с . 1 2 . 4.

П ростой маятник

Здесь m - масса груза в маятнике, g ускорение силы тяжести. Угол Е> это угол смещения груза относительно вертикали (см. рис. 1 2 . 4). Для простых гармонических колебаний длина дуги, по которой движется груз к вертикали, обозначается х. Эту длину можно представить в виде -

-

Здесь L - длина нити, на которой закреплен груз (см. рис. 1 2 .4). Для маленьких углов е значение sin е приближенно равно е, поэтому можно преобразовать предыдущее равенство и подставить результат в выраже­ ние для силы: х

F = - mg L

Это равенство можно слегка видоизменить: F=

mg

-- х

L

Если длина нити (L) не изменяется, то величина (mg / L) тоже будет постоянной. Обозначим эту величину k . При этом мы получим формулу

F =

-

kx

31 5

Системы м а сс и пруж и н

З а кон Гука Закон Гука - это простое выражение для вычисления силы, с которой пружина стремится вернуть прикрепленную к ней массу в равновесное положение: F = -kx Но это же формула для гармонических колебаний маятника! Мы используем эту формулу, поскольку движение масс, прикреплен­ ных к пружинам, носит гармонический характер. Для пружин k не обя­ зательно равно mg / L. Значение k определяется жесткостью пружины. Чем жестче пружина, тем больше усилие, с которым она тянет массу по направлению к равновесному положению. Поэтому чем жестче пружина, тем больше ее значение k. У мягких пружин значение k невелико, по­ скольку развиваемые ими усилия малы.

З а тухающие га рмонические кол еб ания В реальном мире массы, подвешенные к пружинам, не будут колебаться вечно. Колебания будут затухать , поскольку пружины неидеальны и часть энергии будет теряться из-за трения. Кроме того, на систему может влиять сопротивление воздуха или воды . На рисунке 1 2 . 5 показан при­ мер системы, в которой гармонические колебания будут затухать под действием сопротивления воды.

Жесткость

пружины k

Масса

m

Затухание

Ь

Рис. 1 2. 5 . Затухающие гармонические

колебания

Глава 1 2

316

Груз на рисунке 1 2 . 5 подвешен к пружине. Кроме того, к грузу при­ креплена пластинка, погруженная в вязкую жидкость. Эта жидкость обуславливает появление тормозящей силы, пропорциональной скорости движения груза, но обратной по направлению к этой скорости. Поэтому тормозящую силу описывает выражение:

F = -bv Здесь v - скорость движения груза, а Ь коэффициент, связывающий тормозящую силу и эту скорость. Общая сила, действующая на груз, бу­ дет такой: -

F

=

-kx

-

bv

Теоретически этой простенькой формулы достаточно для моделирова­ ния систем масс и пружин. К несчастью, реальность несколько сложнее. Попробуем реализовать в программе фрагмент ткани и разобраться, поче­ му ЭТО СЛОЖНО.

Р еал и з аци я т ка н и Реализовать в программе кусок ткани довольно сложно. Нужно реализо­ вать всю физику материальных точек , образующих ее, и всю физику пру­ жин, связывающих эти материальные точки. Кроме того, нужно знать, как искривлять и деформировать сетчатые модели в Direct3D, чтобы со­ вмещать вертексы моделей с материальными точками. Деформация мо­ делей требует хорошего знания возможностей Direct3D, и в этой книге она не рассматривается. Мы аппроксимируем внешний вид ткани увели­ ченными сетчатыми моделями для отдельных материальных точек. Хотя внешний вид ткани, полученной таким образом, будет не слишком прав­ доподобным, мы сосредоточимся на физике, а не на DirectX.

Зам ечание Чтобы узнать больше о деформациях сетчатых м оделей, попробуйте почи­ тать книгу «Special Effects Game Programming with DirectX» ( издательство Premier Press) . В этой книге есть целая глава, посвяще н н ая деформации изображений. Прочитав эту главу, вы поймете, что нужно сделать, чтобы улуч шить внешний вид ткани в игре.

Усовершенствование материальны х точек Прежде чем мы приступим к реализации ткани, нужно обдумать некото­ рые ее свойства. Например, ткань можно прикрепить к определенным объектам в ЗD-сцене. Хороший пример такой прикрепленной ткани - го­ белен на стене замка. Углы гобелена прикреплены к стене и неподвиж­ ны, а остальная часть может двигаться и развеваться. Вспомните,

Системы м асс и пружи н

317

положение каждого вертекса в сетчатой модели ткани определяется по­ ложением соответствующей материальной точки. Поэтому нужно найти способ обеспечить неподвижность некоторых материальных точек . Кроме того, чтобы ткань выглядела реалистично , нужно, чтобы мате­ риальные точки были невидимыми. На экране должна отображаться сет­ чатая модель ткани. Поэтому для реализации ткани нам понадобятся материальные точки, с которыми не связаны сетчатые модели. Хотя мы и не будем полностью реализовывать ткань, я создал версию класса po­ int_mass, соответствующую этим требованиям. В программе моделирования ткани класс point_mass унаследован от базового класса с помощью механизма наследования языка С++. Код ба­ зового класса приведен в листинге 1 2 . 1 .

За мечание Исходный код программы модели рования ткани н аходится в папке Sour­ ce\Chapter1 2 \Cloth на поставляемом с книгой компакт-диске. Если вы хотите просто увидеть программу в работе, испол няемый файл находится в папке Source\Chapter12\Bin. Листинг 1 2 . 1 . Класс point_mass_base

1 2

class point_mass_base {

З

private : scalar mas s ; vector 3d centerOfМassLocation ; 5 vec tor 3d linearVeloci ty ; 6 vector 3d l inearAcceleration ; 7 vector Зd cons tantForce ; 8 vector 3d impulseForce ; 9 10 scalar radius ; 11 scalar coefficientOfRestitution ; 12 13 Ьооl is immovaЫe ; 14 15 1 6 puЬlic : point_mass_Ьase ( ) ; 17 4

18

19 20 21 22 23 24

25 26

void мass ( scalar massValue) ; scalar мas s (void) ; void Loca tion ( vector_3d locationCenterOfМass) ; vector 3d Location (void) ;

Глава 1 2

318 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 } ;

void LinearVelocity ( vector_3d newVelocity) ; vector_3d LinearVelocity (void) ; void LinearAcceleration ( vector_3d newAcceleration) ; vector_3d LinearAcceleration (void) ; void Cons tantForce ( vector_3d sumCons tantForces) ; vector_3d Cons tantForce (void) ; void ImpulseForce ( vector_3d sumimpulseForces ) ; vector_3d Impul seForce (void) ; void BoundingSphereRadius ( scalar sphereRadius ) ; scalar BoundingSphereRadius (void) ; void Elasticity ( scalar elasticity) ; scalar Elasticity (void) ; void I simmovaЫe ( Ьооl i sМas simmovaЬle ) ; Ьооl I s immovaЬle (void) ; virtual Ьооl Update ( scalar changeinTime) ;

:Класс point mas s base, определение которого приведено в листин­ ге 1 2 . 1 , содерж;;:т по�ти всю функциональность, присутствовавшую в классе point mas s . Но в классе point mass base нет элемента данных типа mesh и мемента данных для хра�rения Глобальной матрицы. Нако­ нец, нет метода Render ( ) . Эти изменения отражают тот факт, что объек­ ты класса point mass base не могут отображаться на экране. :Кроме того, заметьrе , что метод Update () сделан виртуальным, что­ бы его легко было переопределить в производных классах. Далее - теперь можно по отдельности задавать и считывать величины импульсных и по­ стоянно действующих сил, приложенных к объекту. В классе point mass base есть новый элемент данных - is iпunovaЬ­ le. Если значениё этого элемента - true, то соответствующий объект клacca point_mass_base не может двигаться. В класс добавлены соответ­ ствующие методы для чтения и установки значения элемента is iпunovaЬ­ le. Другие методы класса проверяют значение элемента is iпunovaЬle, чтобы выяснить, может ли объект двигаться. В листинге 1 2 . 2 приведен код некоторых методов класса point_mass_base, обращающихся к эле­ менту isiпunovaЬle.

319

Системы м а сс и п ружи н Листинг 1 2 . 2 . П рименение элемента данных isl m m ovaЫe

1 2

inline void point_mas s_Ьase : : LinearVelacity ( vector_3d newVelocity)

3 if ( ! is immovaЬle)

4 5 6 7 8 9

linearVelocity

newVelocity ;

else linearVelocity

10 11

=

vector_3d ( O . o , 0 . 0 , 0 . 0 ) ;

12

13 14 15 inline void point_mass_Ьase : : LinearAcce.1.eration ( vector_3d newAcceleration) 16

1.1

\

if ( ! is immovaЬle) 18 19 linearAcceleration = newAcceler�tion ; 20 21 else 22 23 linearAcceleration = vector_3d ( 0 . 0 , 0 . 0 , 0 . 0 ) ; 24 25 26 27 28 inline void point_mass_Ьase : : Cons tantFo i:ce ( vector_3d swnCons tantForces ) 29 30 31 32

i f ( ! is immovaЬle)

33 34

cons tantForce

=

sumConstantForces ;

else 35 36 cons tantForce = vector_3d ( 0 . 0 , 0 . 0 , 0 . 0 ) ; 37 38 }; 39 40 41 inline void point_mass_base : : ImpulseForce ( vector_Зd sumimpulseForces ) 42 43 if ( ! is iппnovaЬle) 44 45 impul seForce sumimpul seForces ; 46 47 else 48

320 49 50 51 52 } 53

Глава 1 2

irnpulseForce

vector_Зd { 0 . 0 , 0 . 0 , 0 . 0 ) ;

};

Все методы записи значений в классе проверяют значение элемента is iпunovaЬle, чтобы проверить, является ли материальная точка непо­ движной. Если да, то эти методы уменьшают до О линейную скорость ма­ териальной точки, ее линейно е ускорение, постоянные и импульсные силы, действующие на нее . В противном случ:ае в элементы записывают­ ся значения, передаваемые в качестве параметров этим методам. Если материальная точка неподвижна, ее скорость и ускорение должны быть нулевыми, и на нее не могут действовать никакие силы. Таким способом достигается неподвижность точки - изменить ее местоположение можно, только явным образом задав новые координат ы . Если вы хотите, чтобы материальная точ:ЕСа отображалась на экране, не исnол.ьз-уйте объекты кл.асса point m.a.s s base. Вместо этого исполь­ зуйте объекты класса point mas s , коrорый ; своей новой версии являет­ ся производным от point_m ss_Ьase . Определение новой версии класса point_mass приведено в листинге 1 2 . 3 . Л истинг 1 2. З . Новая версия класса point_mэss 1 2 3 4

class point_mass : puЬlic point_mass_Ьa se

{

private : mesh obj ectмesh ;

5 6

DЗDXМATRIX worldМatrix ;

7 В

puЬlic : bool LoadМesh ( 10 s td : : string meshFileName ) ; 9

11 12 13 14 15 16 17 18 } ;

void ShareМesh ( point_mass &sourceМass) ; Ьооl Update ( scalar changeinTime) ; Ьооl Render (void) ;

Теперь определение класса point mass очень короткое. Из листин­ га 1 2 . 3 видно, что этот класс наследуетот класса point_mass_base боль­ шую часть своей функциональности. В самом классе point_mass теперь всего два элемента данных . Первый предназн�чен для хранения сетчатой модели. Второй - для хранения глобальной матрицы. Эти два элемента данных позволяют отображать объекты класса на экране.

Системы м асс и пружи н

321

Поскольку у объектов класса point_mass есть сетчатая модель, кото­ рую можно отобразить на экране, в классе point_mass есть метод Load­ Mesh ( ) , служащий для загрузки этой модели. Кроме того, в классе есть специальный метод ShareМesh ( ) , позволяющий множеству объектов ис­ пользовать одну и ту же сетчатую модель. Версия метода Update ( ) класса point mass переопределяет версию этого метода в классе point mass ьаsе. К роме того, в отличие от класса point_mas s_ьase, в классе ро-: int mass есть метод Render ( ) . Код методов Update ( ) и Render ( ) при­ ведей в листинге 1 2 . 4 . Листинг 1 2 .4. Методы Update() и Render( ) кл асса point_mass

1 Ьооl point_mass : : Update ( scalar changeinTime) 2 3 point_mass_base : : Update ( changeinTime) ; 4 5 / / Создаем матрицу nеремещения . 6 D3DXМatrixTrans lation ( 7 &worldМatrix , 8 Location ( ) . Х ( ) 9 10 Location ( ) . У ( ) , Location ( ) . Z ( ) ) ; 11 12 return ( true) ; 13 14 15 1 6 Ьооl point_mass : : Render (void) 17 { // С охраняем глобальную матрицу nреобразования . 18 D3DXМATRIX saveWorldМatrix ; 19 theApp . D3DRenderingDevice ( ) - >GetTrans form ( 20 D3DTS_WORLD , 21 &saveWorldМatrix ) ; 22 23 // Применяем глобальную матрицу nреобразования 24 / / к данному объекту . 25 theApp . D3DRenderingDevice ( ) - >SetTransform ( 26 D3DTS_WORLD , &worldМatrix) ; 27 28 // После nреобразования вьmолняем рендеринг объекта . 29 Ьооl renderedOK=obj ectмesh . Render ( ) ; 30 31 // Восстанавливаем глобальную матрицу nреобразования . 32 33 theApp . D3DRenderingDevice ( ) - >SetTransform ( D3DTS_WORLD , 34 &saveWorldМatrix ) ; 35 36 return ( renderedOK) ; 37 38 ,

3 22

Гл ава 1 2

Метод Update ( ) класса point mass теперь делает куда меньше, чем раньше. Он вызывает метод point=:mass_base : : Update ( ) в строке 4 ли­ стинга 1 2 . 4 , и вызванный метод выполняет все физические расчеты по­ ступательного движения. По завершении этих расчетов метод point_ mas s : : Update ( ) создает матрицу перемещения объекта point_mas s по координатам, вычисленным методом point mass_base : : Update ( ) . _ Как и в предыдущих версиях метода Render ( ) , в этой версии гло­ бальная матрица сохраняется во внутрен:ней переменной, а затем Direct3D передается матрица перемещения, :хранящаяся в объекте клас­ са point_ mas s . Затем выполняется рендеринг сетчатой модели объекта, после чего восстанавливается из внутренней переменной ранее сохранен­ ная глобальная матрица. Наличие классов point_mass_base и po int_mass позволяет нам со­ здавать как видимые на экране материальнъ:rе точки, так и невидимые. Если мы создаем в программах невидимые м�териальные точки, неплохо было бы дать их классу имя более подходящее, чем point_mass_base. Поэтому в программу включен оператор typedef, задающий новое имя классу point_mass_Ьase - invisiЫe__point_mas s . При этом просто меняется имя класса - его функциональность остается той же.

П ружины Чтобы реализовывать в играх волосы и тк ань, нам нужно моделиро­ вать пружины. В идеале модель пружины должна вести себя так же, как реальные пружины . Программа должна иметь возможность зада­ вать жесткость пружины (k) и коэффициент затухания (Ь) и использо­ вать выведенные ранее в этой главе уравнения поведения пружин, чтобы модели вели себя так же, как реальные пружины . К несчастью, на самом деле моделировать пружины не так просто. Одна из основных сложностей - уравнени.я поведения пружин не пол­ ностью описывают поведение реальных пружин. Эти уравнения подразу­ мевают, что невозможно сжать или растян:уть пружину так , как это невозможно в реальности . Если это сделать, :реальные пружины ломают­ ся или теряют упругость . При этом материал:ьные точки, прикрепленные к ним, могут двигаться хаотично и непредск�зуемо. Если это произойдет в программе, то фрагменты ткани или волос могут двигаться непредска­ зуемо, часто деформируясь или беспорядочно изменяя размер. Один из способов решения этой проблемьr - ввести пределы сжатия и растяжения в модели пружин. Это звучит просто, но на самом деле это довольно сложная задача программированип:. Если пружина растянута или сжата до предела, она должна ограничи:вать перемещения прикреп­ ленных к ней материальных точек, и, соответственно, влиять на сжатие или растяжение других пружин. Вообще, перемещение любой материаль­ ной точки в системе масс и пружин приведет к волне изменений положе­ ния во всей системе. Конечно, в реальности это так и есть - если мы тянем на себя скатерть, она двигается вся цеJiиком, но смоделировать та­ кое поведение довольно сложно.

Системы м асс и п руж и н

323

Можно задать пределы сжатия и растяжения не в классе пружины spring. Это нужно делать на уровне системы масс и пружин, то есть в классе ткани - cloth. Подстройки, которые должен выполнять класс cloth, слож­ ны и часто зависят от конкретных ситуаций. Поэтому класс cloth будет ма­ лопригоден к широкому примененmо. Лучше найти более простой подход.

-

Листи н г 1 2 . 5 . Простой кл асс spring

1 class spring 2 { 3 private : 4 scalar restLength ; scalar forceCons tant ; 5 scalar dampeningFactor ; 6 7 8 point_mass_Ьase *pointмas s l ; point_mass_Ьase *pointмas s2 ; 9 10

1 1 puЬlic : spring ( ) ; 12 13

1;� 15 16 tЧ18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 } ;

void Length ( scalar springLength) ; scalar Length (void) ; void ForceConstant ( scalar springForceConstant) ; scalar ForceConstant (void) ; void DampeningFactor ( scalar dampeningConstant) ; scalar DampeningFactor (void) ; void EndpointМas s l ( point_mass_Ьase *particlel ) ; point_mass_Ьase *Endpointмas s l (void) ; void EndpointМass2 ( point_mas s_base *particle2 ) ; point_mass_Ьase *Endpoint.Мas s2 (void) ; Ьооl IsDisplaced (void) ; void CalculateReactions ( scalar changeinTime ) ;

Глава 1 2

324

Вместо того чтобы задавать пределы сжатия и растяжения , можно стабилизировать системы масс и пружин, применяя дополнительные силы, гасящие колебания. Эти силы предотвращают хаотичное движение частиц - колебания быстро затухают, и система замирает. Такое решение просто реализовать в программах, и оно не требует больших объемов до­ полнительных вычислений для использования, поэтому оно широко при­ меняется. Дополнительное затухание вводится на уровне всей системы, то есть в классе cloth, а не spring. Причина этого - в разных типах сис­ тем масс и пружин используются разные виды затухания. Сам класс spring не слишком сложен. Определение этого класса при­ ведено в листинге 1 2 . 5 . В классе spring объявлены пять элементов данных. В первом и з них хранится длина пружины в состоянии покоя, то есть не сжатой и не рас­ тянутой. В строках 5-6 листинга 1 2 . 5 определены элементы данных для хранения характеристик k и Ь пружин . Кроме того, в классе spring объявлены указатели на материальные точки, к которым крепятся пружины. Обратите внимание - это указатели на тип point_mas s_Ьase . Это позволяет нам прикреплять пружины как к видимым, так и к невидимым материальным точкам. Проще говоря, ука­ затели, объявленные в строках 8-9 листинга 1 2 . 5, могут указывать на объ­ екты как класса point_mass, так и класса invisiЬle_J?oint_mas s . В строках 1 4 - 3 2 определены методы чтения и записи значений в эле­ менты данных. Метод I sDisplaced ( ) определяет, растянута пружина или сжата. Метод CalculateReactions ( ) вычисляет силу, приклады­ ваемую пружиной к материальным точкам, к которым она прикреплена. Код этих двух методов приведен в листинге 1 2 . 6 . Л истинг 1 2 . 6 . Рабочие методы класса spri ng

1 2 З 4 5 б

7 8 9 10 11 12 13 14 15 16 17

bool spring : : I sD isplaced (void) {

assert (pointмas s l ! =NULL) ; assert (pointмass2 ! =NULL) ; bool isDisplaced

=

false ;

vector_Зd currentLength ; / * Находим расстояние между частицами , х хоторым прихреплена пружина . * / currentLength = pointмas s l - >Location ( ) - pointмass2->Location ( ) ; / * Находим разность между .цлиной пружины в даню.�й .мо.меит и е е ДЗIИНОЙ в состоянии похоя . * / scalar lengthDifference =

Системы м а сс и пружи н

325

18 currentLength . NormSquared ( ) - ( restLength*restLength) ; 19 / / Есnи разность заметно отличается от О . . . 20 21 if ( ! CloseToZero ( lengthDifference) ) 22 23 isDisplaced=true ; 24 25 return ( isDisplaced) ; 26 27 28 2 9 void spring : : CalculateReactions ( scalar changeinTime ) 30 31 32 assert (pointмas s l ! =NULL) ; as sert (pointмass2 ! =NULL) ; 33 34 35 vector_3d currentLength ; 36 // Находим длину пружины в данный момент . 37 currentLength = 38 pointмas s l - >Location ( ) - pointмas s2->Location ( ) ; 39 40 / / Преобразуем е е в схаляр . 41 scalar currentLengthМagnitude = currentLength . Norm ( ) ; 42 43 / * Находим разность между длиной пружины в данный момент 44 и ее длиной в состоянии похоя . * / 45 46 scalar changeinLength=currentLengthМagnitude-restLength ; 47 48 / / Если изменение длины прахтичесхи нулевое . . . 49 50 if ( CloseToZero ( changeinLength ) ) 51 // Уменьшаем его до О . 52 53 changeinLength=0 . 0 ; 54 55 // Находим величину силы , развиваемой пружиной . 56 scalar springForceМagni tude = 57 58 forceConstant * changeinLength ; 59 / / Находим величину гасящей силы в пружине . 60 scalar dampeningForceМagni tude ; 61 if ( changeinTime . Of) 62 63 64 dampeningForceМagnitude

Глава 1 2

326 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94

ciampeningFactor * changeinLength * changeinTime ; else dampeningForceМagnitude = dampeningFactor * changeinLength / changeinTime ;

// Гасящая сила всегда меньше силы , развиваемой пружиной . if ( dampeningForceмagnitude springForceмagnitude) dampeningForceМagnitude

=

springForceМagnitude ;

/ / Уменьшаем силу , развиваемую пружиной . scalar responseForceмagnitude springForceМagnitude - dampeningForceМagni tude ; =

// Преобразуем силу , развиваемую пружиной , в вектор . vector_3d responseForce responseForceмagnitude * currentLength . Normalize ( SCALAR_TOLERANCE ) ; =

// При:кл а;цываем развив аемую пружиной силу / / к материальным точкам . pointмassl ->ImpulseForce ( pointмas s l - >ImpulseForce ( ) + - l *responseForce) ; pointмas s2 ->ImpulseForce ( pointмas s2->ImpulseForce ( ) + responseForce) ;

Метод I sDisplaced ( ) находит длину пружины в данный момент, вычитая векторы местоположения двух материальных точек, к которым эта пружина прикреплена. Магнитуда вектора, получающегося в резуль­ тате вычитания, будет равна расстоянию между этими двумя материаль­ ными точками, то есть длине пружины. В строках 1 7- 1 8 метод I sDisplaced ( ) находит магнитуду или норму вектора. Чтобы найти норму, нужно вычислить квадратный корень. Дабы избежать этого, метод I sDisplaced ( ) использует квадрат нормы. Он вычитает квадрат длины пружины в состоянии покоя из :квадрата нор­ мы. Этого достаточно, поскольку на самом деле знать расстояние между двумя материальными точками методу не нужно. Ему нужно только знать, сжата пружина или растянута. Применение квадратов норм вместо норм позволяет это определять. Метод CalculateReactions ( ) , начинающийся в строке 29 листин­ га 1 2 . 6 , вычисляет силу, которую прикладывает пружина к материальным

Систе мы м а сс и п ружи н

327

точкам, к которым она прикреплена. Первый шаг, необходимый для вы­ числения этой силы, - найти длину пружины в данный момент. Это дела­ ется в строках 38-39. В отличие от метода I sDi splaced ( ) , метод CalculateReactions ( ) не может обойтись квадратом длины и вынуж­ ден вычислять квадратный корень.

Зам е чание Вероятно, вы заметили, что метод, просчитывающий действия пружин, назы­ вается CalculateReactions ( ) , а не Update ( ) , как аналогичные по назначе­ нию методы других классов в платформе физического моделирования. Это решение основывается на моей л ичной точке зрени я . Поскольку пружина н икогда не отображается на экране, и единственное, что она делает - при­ кладывает силы к другим объектам , я посчитал, что она существенно отл и ­ чается о т других модели руемых объектов, и методу нужно дать другое и м я .

Далее метод Calcula teReactions ( ) вычисляет магнитуду вектора, определяющего длину пружины в данный момент. В строке 4 7 он нахо­ дит разность между этой длиной и длиной пружины в состоянии покоя. Эта разность умножается на жесткость пружины в соответствии с форму­ лой F = -k x , где х - изменение длины пружины относительно равновес­ ного состояния. Гасящая сила вычисляется в строках 6 1 - 7 1 . Вычисленная величина силы, которую развивает пружина, может оказаться меньше, чем вели­ чина гасящей силы. Так ли это, зависит от значений жесткости пружины (k) и коэффициента затухания (Ь). Если коэффициент затухания велик по сравнению с жесткостью пружины, то сила затухания может превзойти силу, развиваемую пружиной. Однако это невозможно с физической точки зрения. Поэтому оператор if в строках 74- 7 7 гарантирует, что гасящая сила не превысит силы, развиваемой пружиной. Это в значительной степе­ ни гарантирует устойчивость моделируемой системы масс и пружин. В строках 80-81 метод CalculateReactions ( ) находит итоговую силу, прикладываемую пружиной к материальным точкам, к которым эта пружина прикреплена. В строках 84-86 величина этой силы преобразуется в вектор . Метод CalculateReactions ( ) заканчивается прикладыванием этой силы (в противоположных направлениях) к обеим материальным точ­ кам в качестве импульсной силы. Вот, собственно говоря, и все, что нам понадобится для моделирова­ ния пружин в программе. Однако, как уже говорилось выше, придется сделать еще кое-что, чтобы пружины оставались устойчивыми.

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

Глава 1 2

328

в программе. Как вы , вероятно, ожидали, реализация их в коде программы куда сложнее. В листинге 1 2 . 7 приведено определение описы­ вающего ткань класса cloth. Л истинг 1 2 . 7 . Оп ределение класса cloth

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

class cloth / / Внутренние типы private : enwn cloth constants PARTICLES_PER_SQUARE=4 , TOP_LEFТ_PARTICLE=O , TOP_RIGHT_PARTICLE , BOTTOM_LEFT_PARTICLE , BOTTOM_RIGHT_PARTICLE , TOP_SPRING = О , BOTTOM_SPRING , RIGHT_SPRING , LEFT_SPRING , TOP_RIGHT_TO_BOTTOM_LEFT_SPRING , TOP_LEFT_TO_BOTTOM_RIGHT_SPRING , SPRINGS_PER_SQUARE=6 , };

struct index_pair int row , col ; }; struct cloth_square index_pair particleindex [ PARTICLES_PER_SQUARE ] ; int springindex [ SPRINGS_PER_SQUARE] ; }; / / Рrivаtе-элементы данных . private : int totalRows ; int totalCols ; int totalSprings ; point_mass * * allParticles ; spring * allSprings ; cloth_square * *allSquares ; scalar linearDampeningCoefficien t ;

Систем ы м а сс и пружи н 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82

329

// Рrivаtе-методы private : void cloth : : HandleCollision ( vector_3d separationDistance , scalar changeinTime , index_pair f irs tParticle , index_pair secondParticle ) ; / / РuЫiс-методы puЬlic : cloth ( int particleRows , int particleCols , scalar particleМa.ss , scalar particleRadius , scalar particleElas ticity , scalar spaceBetweenParticles , scalar clothSti ffnes s , scalar dampeningFactor , scalar linearDampeningFactor , vector_3d upLeftCorner) ; void ParticleimpulseForce ( int row , int col , vector_3d impulseForce) ; vector_3d Particleimpul seForce ( int row , int col ) ; void ParticleCons tantForce ( int row , int col , vector_3d constantForce) ; vector_3d ParticleCons tantForce ( int row , int col ) ; void I sParticleinnnovaЬle ( int row , int col , bool isмass immovaЬle) ; bool I s ParticleinnnovaЬle ( int row , int col ) ; bool LoaclМesh ( std : : string meshFileName ) ; bool Update (scalar changeinTime) ; bool Render (void) ; };

Функциональность класса cloth обширнее, чем требуется в програм­ ме примера из этой главы. Этот класс делит ткань на квадратные участ­ ки. Углы каждого такого участка - это четыре материальные точки в системе масс и пружин . Такое представление ткани дает вам ряд преи­ муществ. Во-первых, можно добавлять собственный код в методы клас­ са cloth, чтобы выполнять специальные операции над отдельными

330

Глава 1 2

участками ткани. Это, вероятно, потребуется вам, если вы будете серьез­ но работать с тканью в играх. Во-вторых, разделение ткани на квадраты дает возможность применять отдельную сетчатую модель к каждому та­ кому квадрату, а не использовать одну сетчатую модель для всего полот­ на ткани. В результате можно широко варьировать внешний вид ткани. Чтобы упростить разделение ткани на квадраты, в классе cloth определен ряд внутренних типов и констант . Все :константы включены в перечисление cloth cons tants, определенное в строках 5 - 1 9 лис­ тинга 1 2 . 7. Эти :константы задают количество материальных точек в квадрате (конечно, 4) и позицию каждой материальной точки в :квад­ рате . Кроме того , :константы задают количество и расположение пру­ жин, соединяющих материальные точки. Тип index_J? air позволяет легко задавать строки и столбцы матери­ альных точек и квадратов. Он используется в типе cloth square , опре­ деленном в строках 26-30 листинга 1 2 . 7. Каждый экземпляр типа cloth_square содержит массив элементов типа index_J? air (строка 28 листинга). Этот массив хранит номера строк и столбцов для каждой из материальных точек в квадрате. Кроме того, в типе cloth_square опреде­ лен целочисленный массив. В нем хранятся индексы пружин в квадрате. Если вы посмотрите еще раз на рисунок 1 2 . 2, то увидите :квадраты, образующие ткань, и заметите, что и материальные точки, и пружины входят в состав нескольких квадратов. Поэтому хранить их в типе cloth_ square бессмысленно - это приведет к их дублированию и на­ прасной трате памяти. Рrivаtе-элементы данных класса cloth объявлены в строках 34-40 листинга 1 2 . 7. В этих элементах данных хранится :количество строк и столбцов в системе, а также общее :количество пружин в ней. В строке 37 объявлен указатель на указатели на объекты point_ mass. Его использование позволяет классу cloth динамически выделять под ткань произвольное количество материальных точек . Как вы вскоре увидите, этот указатель используется в конструкторе для динамического выделения двумерного массива.

Зам е ча ние Не забывайте - класс cloth в п рограмме использует масс и в объектов класса point_mas s , а не invisiЫe_point_mass , поскольку мы не будем связываться с деформацией сетчатых м оделей. Вместо этого мы отобра­ з и м ткань в виде набора сетчатых моделей материальных точек. Это будет не слишком реалистично, но если вы хотите получить реалистично выгля­ дящую ткань, то должны уметь выполнять деформацию сетчатых м оделей и использовать в классе cloth массив объектов класса invis iЫe_po­

int mass .

Кроме того, в классе cloth есть указатель на пружины. Этот указатель используется для работы с одномерным динамически выделяемым масси­ вом пружин. Поскольку квадраты ткани расположены в определенных

Системы м а сс и п ружи н

33 1

столбцах и строках, то в классе cloth есть указатель на указатели на структуры cloth_square (строка 39). Этот указатель используется для ра­ боты с динамически выделяемым двумерным массивом :квадратов ткани. В классе cloth есть также ря:д методов как доступных извне, так и предназначенных для внутреннего использования. Начнем их рассмотре­ ние с конструктора.

И нициал изация фра гм ента ткан и Инициализация фрагмента ткани: - довольно сложный процесс. Его вы­ полняет конструктор класса cloth, код которого приведен в листин­ ге 1 2 . 8. Листинг 1 2 . 8 . Конструктор класса cloth

1 cloth : : cloth ( int particleRows , 2 int particleCol s , 3 scalar particleМas s , 4 scalar particleRadius , 5 scalar particleElasticity , 6 7 scalar spaceвetweenParticles , scalar clothStiffness , 8 scalar dampeningFactor , 9 scalar linearDampeningFactor , 10 vector_3d upLeftCorner) 11 12 assert (particleRows=2 ) ; 13 14 as sert (particleCols=2 ) ; 15 l inearDampeningCoefficient = linearDampeningFactor ; 16 17 // Вы,целяем память nод о;цио измерение массива 18 / / материальных точек . 19 allParticles = new point_mass * [particleRows ] ; 20 21 / / Е е.пи память выделить н е удалось . . . 22 i f ( al lParticles==NULL) 23 24 / / Генерируем искточение . 25 pmlib_error outOfМemory ( 26 27 " Can ' t allocate memory for cloth . " ) ; throw outOfМemory ; 28 29 30 31 // // Вы.целяем память под второе измерение массива . 32

332 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 7О 71 72 73 74 75 76 77 78 79

Глава 1 2 // int i , j ; / / Для JСаждой строJСи . . . for ( i=O ; iCreateDevice ( GUID_SysMouse , &mouseDevice , NULL) ; 11 Если произоШJiа ошибка , а мышь необходима игре . . . if ( (result ! =DI_OK) && (directinputParams . requiresMouse) ) {

noError = false ;

return (noError ) ;

В листинге 14. 1 7 приведен код нового методв. из платформы. Этот код на­ ходится в файле РМDЗDАрр . срр. Метод Ini tDirectinput ( ) вызывает функ­ цию Directinput8Create ( ) , чтобы создать интерфейс IDirectinput8. Параметры функции Directinput8Create ( ) объяснены в таблице 14.1. Четвертый параметр функции Directinput8Create ( ) - это адрес переменной-указателя. Именно в этом пара:м:етре D irectinput8Crea­ te ( ) сохраняет указатель на интерфейс IDirectinput8 . Теперь в классе платформы dЗd арр есть указатель на интерфейс IDirectinput8 - это переменная из строки 1 1 листинга 1 4 . 1 7 . Игра может обратиться к ин­ терфейсу IDirectinputS в любой момент, вызвав метод dЗd_арр : : Di­ rectinputDevice ( ) . _

Гот о в имся создавать и г ры

403

Таблица 1 4. 1 . Параметры функции Directlnput8Create ( ) Имя параметра

Описание

hinst

Дескриптор текущего модул я . Просто присвойте этому параметру значение, возвращаемое функцией

GetмoduleHandle (NULL ) dwVersion

Присвойте этому параметру значение

DIRECTINPUТ_VERS ION riidltf

Идентификатор и нтерфейса, позволяющий использовать кодовые таблицы ANSI или Uпicode. Я рекомендую присвоить ему значение IID IDirectinputB , чтобы использовались кодовые таб.ilицы, задан ные в настройках ком п илятора. Это сильно упрощает жизнь

ppvOut

Адрес, по которому находится указатель на интерфейс. Именно сюда функция Directinput8Create ( ) выводит адрес такого указателя для вашей программ ы

punkOuter

Указател ь, используемый для особо изощре н ной возможности СОМ-объектов, называемой агрегированием (aggregation ) . Если вы не знаете, что это такое, считайте, что вам повезло. Установите этот параметр в значение N U LL и забудьте о нем. В л юбом случае , вы не будете его испол ьзовать - этот механизм в СОМ оооочень медленно работает, и использовать его в играх невозможно

Зам ечание П оскол ьку теперь платформа содержит функци и , испол ьзующие дополни­ тел ьные компоненты DirectX, в частности, Directlnput, я внес в нее некото­ рые изменения . Теперь больши нство функций в файле РМDЗDАрр . срр я вляются рrivаtе-элементами класса dЗd арр. Именно поэтому функция Ini tDirectinput ( ) включена в класс d°Зd_app. Так мы получим более удобную структуру инициализации дополнительных компонентов DirectX.

Если создание интерфейса IDirectinput8 прошло без ошибок, метод Ini tDirectinput ( ) проверяет содержимое элемента данных directin­ putParams , чтобы выяснить, требуется ли поддержка клавиатуры. Если да, то метод создает интерфейс IDirectinputDeviceS для клавиатуры. Если создание интерфейса для клавиатуры привело к ошибке, а кла­ виатура необходима игре, то метод Ini tDirectinput ( ) возвращает ошибку. Если же без клавиатуры можно обойтись, то метод продолжает выполняться, как ни в чем не бывало. Точно так же создается интерфейс IDirectinputDevice8 для мыши.

404

Глава 1 4

Теперь, когда у игры есть указатели на интерфейсы для устройств ввода, ей необходимо задать форматы получаемых от этих интерфейсов данных. Кроме того, если игра использует буферизованный ввод, она должна задать емкости буферов . Обе эти операции выполняются в методе Gameini tiali za tion ( ) класса игры или в методе, вызываемом из этого метода. Код такого метода п риведен в листинге 1 4 . 18. Листинг 1 4. 1 8 . Завершение инициализации устройств ввода 1 void my_game : : SetupinputDevices ( ) 2 { 3 D IPROPDWORD dipdw ; 4 5 if ( theApp . Keyboard ( ) ) 6 { 7 theApp . Keyboard ( ) ->SetDataFormat ( &c_dfDIKeyboard) ; 8 9 dipdw . diph . dwSize = si zeof (DIPROPDWORD ) ; 10 dipdw . diph . dwHeaderSize = si zeof (DIPROPHEADER) ; dipdw . diph . dwObj = О ; 11 12 dipdw . diph . dwHow = D IPH_DEVICE ; 13 dipdw . dwData = КEYВOARD_BUFFER_SIZE ; 14 15 / * В этом примере ошиб JСИ , возвращаемые фунJСЦИей , 16 никак не обрабатываются . Еспи произошла ошибка , значит , 17 с Юiавиатурой серьезные проблемы . Иrра должна сообщить 18 о б этом игроку и аккуратно заверШИ'l'ься . * / 19 theApp . Keyboard ( ) ->SetProperty ( 20 D IPROP_BUFFERSIZE , 21 &dipdw . diph ) ; 22 23 if ( theApp . Mouse ( ) ) 24 25 { theApp . Mouse ( ) ->SetDataFormat ( &c_dfDIMouse) ; 26 27 dipdw . diph . dwSize = sizeof (DIPROPDWORD ) ; 28 dipdw . diph . dwHeaderSize = si zeof (DIPROPНEADER) ; 29 dipdw . diph . dwObj = О ; 30 31 dipdw . diph . dwHow = D IPH_DEVICE ; 32 dipdw . dwData = MOUSE_BUFFER_SIZE ; 33 34 /* В этом примере ошибJСИ , возвращаемые фунJСЦИей , 35 никак не обрабатываются . Еспи произошла ошибка , значит , с МЪIШЬю серьезные проблемы . Иrра должна сообщить 36 37 об этом игроку и аккуратно завершиться . * / theApp . Mouse ( ) ->SetProperty ( 38 39 DIPROP_BUFFERSIZE , 40 &dipdw . diph) ; 41 42

Гото в имся создавать игры

405

В листинге 14. 18 приведен код метода, завершающего инициализацию клавиатуры и мыши. Этот метод должен вызываться из метода Gameini­ tialization ( ) . Метод SetupinputDevices ( ) задает формат данных для клавиатуры, вызывая метод IDirectinputDevice8 : : SetDataFormat ( ) . Он передает методу SetDataFormat ( ) глобальную переменную, опреде­ ленную в Directinput. Эта глобальная переменная - одна из набора, определенного в Directlnput для настройки форматов данных устройств ввода. Эти переменные описаны в таблице 1 4 . 2 .

Таблица 1 4. 2 . Форматы данных устройств ввода Имя п еременной

Оп иса н и е

c_dfDIKeyboard

Стандартный формат данных для клавиатуры. Создает массив из 256 байтов - по одному на каждую клавишу клавиатуры и еще несколько

c_dfDIMouse

Стандартный формат данных для м ы ш и . Когда программа получает данные от м ы ш и , она помещает их в структуру D IМOUSESTATE, содержащую 4 байта, описы вающую состояние кнопок мыши

с dfDIMouse2

Расширенный формат данных для мыши. Когда программа получает данные от мыши, она помещает их в структуру DIMOUSESTATE2 , содержащую 8 байтов, описывающую состояние кнопок мыши

c_dfDIJoystick

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

DIJOYSTATE c_dfDIJoystick2

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

После того, как метод SetupinputDevices ( ) задаст формат данных для клавиатуры в строке 7 листинга 1 4 . 1 8 , он задает размер буфера для буферизованного ввода (в строках 9-21). Если ваша игра будет использо­ вать небуферизованный ввод, то эту часть метода в ней можно пропус­ тить. В строках 28-40 листинга 14. 18 видно, как задается размер буфера для буферизованного ввода с мыши.

П одс ка зка Инициализация джойстиков и других устройств ввода выполняется практи­ чески по такой же схеме, что и показанная в методах Ini tinputDevices ( ) и SetupinputDevices ( ) Основное отличие для этих устройств - то, что программа должна перечислить их кнопки, ползунки и оси перемещения. .

Кроме того, платформа предоставляет вам функцию, позволяющую опрашивать устройства ввода. Давайте посмотрим, как это делается.

Глава 1 4

406

П олучение дан н ы х от клавиатуры и мыши Если ваша программа использует небуферизованный ввод, она должна вызывать метод IDirectinputDevice8 : : GetDeviceSta te ( ) . Если ис­ пользуется буферизованный ввод, нужно вызывать метод IDirectin­ putDevi ce8 : : GetDeviceData ( ) . В листинге 1 4 . 1 9 показана работа с буферизованным вводом. Листинг 1 4. 1 9 . Метод process_input(} 1 bool my_game : : Proce s sinput ( ) 2 { DIDEVICEOBJECTDATA keyboardData [КEYBOARD_BUFFER_SIZE ] ; 3 D IDEVICEOBJECTDATA mouseData [MOUSE_BUFFER_SI ZE ] ; 4 5 НRESULT resul t ; DWORD totalElements ; 6 DWORD i ; 7 8 totalElements = КEYBOARD_BUFFER_SIZE ; 9 10 resul t = theApp . Keyboard ( ) - >GetDeviceData ( 11 sizeof ( DIDEVICEOBJECTDATA) , keyboardData , 12 13 &totalElements , 14 О) ; 15 i f ( result ! = D I_OK) 16 17 { resul t = theApp . Keyboard ( ) ->Acquire ( ) ; 18 while ( result == DIERR_INPUTLOST ) 19 20 { 21 result = theApp . Keyboard ( ) ->Acquire ( } ; 22 } 23 / * Если приоритет принадлежит другому приложению или 24 25 произошла еще кахая-то ошибха . . . * / if ( ( resu l t - D IERR_OTHERAPPНASPRIO) 1 1 26 ( resul t DIERR_NOTACQUIRED ) ) 27 28 29 / / Просто попробуем еще раз позже . return ( true) ; 30 31 32 33 else 34 bool cameraМoved = fal se ; 35 36 37 for ( i=O ; iGetDeviceData ( sizeof (DIDEVICEOBJECTDATA) , mouseData , &totalElements , 0) ; / * С помощью мыши в программе не ВЫПОЛИЯЮ'L'СЯ нmcaJGre действия . Код приведен здесь в демонсо:rрациоиных целях . * / i f ( result ! = DI_OK) result = theApp . Mouse ( ) ->Acquire ( ) ; while ( result == DIERR_INPUTLOST ) { result = theApp . Mouse ( ) - >Acquire ( ) ;

/ * Если приоритет принадлежит другому приложеИИJО или произошла еще ха:кая-то ошиб:ка . . . * / if ( ( result D IERR_OTHERAPPНASPRIO ) 1 1 (result == DIERR_NOTACQUIRED ) )

408 88 89 90 91 92 93 94 95

Глава 1 4

// Просто попробуем еще раз позже . return ( true ) ;

return ( true) ;

Метод, приведенный в листинге 1 4 . 1 9 - это еще один из методов клас­ са игры . Прежде чем он вызовет метод UpdateFrame ( ) , платформа вызы­ вает метод Proces sinput ( ) . Это позволяет игре реагировать на действия пользователя, прежде чем обновлять кадр соответственно смоделирован­ ной физике. Каждому буферизован::ному устройству ввода требуется отдельный буфер. Метод Processinput ( ) объявляет буфера ввода для клавиатуры и мыши в строках 3-4 листинга 14. 1 9. Размеры этих буферов должны быть как мини­ мум равны размерам, которые игра передает функции SetProperty ( ) из Directlnput. Вспомните, что функция SetProperty ( ) вызывалась в лис­ тинге 1 4 . 18. Массивы, объявленные в строках 3-4 листинга 1 4 . 1 9 - это массивы структур D IDEVICEOBJECTDATA. Чтобы извлечь данные из буфера клавиатуры, из метода Proces­ sinpu t () вызывается метод IDirectinputDevice8 : : GetDevi ceDa­ ta ( ) в строках 1 0 - 1 4 . Первый параметр метода GetDeviceData ( ) - это количество байтов в структуре DIDEVICEOBJECTDATA. Второй параметр это адрес начала буфера клавиатуры, созданного в строке 3 листинга. Третий параметр метода GetDevi ceData ( ) - это общее количество структур в буфере. Когда метод GetDevi ceDa ta ( ) заканчивает выполня­ ться, в третьем параметре содержится действительное количество собы­ тий ввода, записанных в буфере. Последний параметр - это набор флагов. Единственный флаг, распо­ знаваемый в данный MOlVIeнт - это DIGDD РЕЕК, который позволяет ва­ шей программе искать данные в буфере, не удаляя их. Последующие вызовы GetDevi ceDa ta ( ) в этом случае будут возвращать те же данные. Обычно игры сбрасывают этот параметр в О, и данные удаляются из буфе­ ра при их считывании. Если метод GetDeviceData ( ) не может прочитать данные с клавиа­ туры, это обычно означает, что к ней потерян доступ. Это событие похоже на потерю графических поверхностей, но, к счастью, восстановить доступ к устройству ввода проще, чем восстановить графические поверхности. Все, что нужно для этого - вызвать метод IDirectinputDevi ce8 : : Ac­ quire ( ) , как показано в строке 18 листинга 1 4 . 19. Возможно, метод IDi rectinputDevice8 : : Acquire ( ) не сможет восстановить доступ к устройству ввода. Придется немного подождать. В этом случае в строке 1 9 метод Processinput ( ) начинает выполнять цикл while, ожидая, когда доступ будет восстановлен. Если доступ невоз-

409

Гото в и мся создавать игры

можно восстановить из-за какой-то другой ощибки, метод Processin­ put () возвращает значение true и ожидает начала обработки следующего кадра, чтобы еще раз попытаться получить доступ к устройству ввода. Когда метод Processinput ( ) получает буферизованный ввод с кла­ виатуры, он начинает выполнять цикл for (строка 3 7 листинга 1 4 . 1 9) . Цикл перебирает п о очереди все события, которые метод GetDeviceDa­ ta ( ) передал методу Processinput ( ) . Идентификатор каждой нажатой клавиши содержится в структуре DIDEVICEOBJECTDATA в элементе dwOfs. В примере из листинга 14. 1 9 обрабать�ваются нажатия на клави­ ши перемещения курсора (стрелки вверх, вниз, влево и вправо). Листинг 1 4.20. Восстановление доступа к клавиатуре и мыши

после переключения между программами 1 2



4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

bool my_game : : HandleMessage ( НWND hWnd, 1:.JI.NT w.sg ,

WPARAМ wParam , LPARAМ lParam) switch (msg) case WМ ACTIVAТE : / / Еспи мы полуЧаем фокус . . . if (WA_INACTIVE ! = wParam) / / Если клавиатура быnа настроена . . if ( theApp . Keyboard ( ) )

.

{

// Удостоверяемся , qто доступ восстановлен theApp . Keyboard ( ) ->�cquire ( ) ;

/ / Еспи МЪIШЬ быnа настроена . . if ( theApp . Mouse ( ) ) { // Удостоверяемся , qто доступ восстановлен theApp . Mouse ( ) ->Acq\lire ( ) ; .

break ;

return ( false) ;

Буферизованный ввод от мышей и джойстiсiков обрабатывается почти так же, как и от клавиатуры.

Глава 1 4

410 Подска зка

Идентифи каторы, которые программа получает во входном буфере, разли­ чаются для клавиатур, мышей и джойстиков. Список идентификаторов кла­ виатуры можно найти в документации по DirectX в разделе «Keyboard Device Eпumerated Туре» . Список идентификаторов м ыши находится в раз­ деле « Mouse Device Eпumerated Туре», а идентификаторов джойстика - в разделе «Joystick Device Constants» .

Единственное, на что еще стоит обратить внимание - то, что устройст­ ва ввода часто теряются, если игрок нажимает Alt+Tab для переключения в другую программу. Вы можете обнаружить, что игра выполняется эф­ фективнее, если она пытается вновь получить доступ к устройствам ввода каждый раз, когда игрок опять переключается на нее. В листинге 1 4 . 20 приведена версия метода HandleМessage ( ) класса игры, который восста­ навливает доступ к клавиатуре и мыши при реактивации игры.

З авершение работы с Directlnput Прежде, чем завершить выполнение программы игры, нужно освободить все созданные в ней интерфейсы Directlnput. Это делается в методе Ga­ meCleanup ( ) . Вам не придется освобождать интерфейсы клавиатуры и мыши - платформа делает это автоматически.

Перемещение ка меры

в

DirectX

Последняя тема, которую нужно рассмотреть, прежде чем мы приступим к написанию похожих на игры программ, - это перемещение камеры, то есть точки, из которой игрок видит 3D-сцену. В DirectX оно реализуется довольно просто. Собственно говоря, вы уже знаете, как это делается. Взгляните на листинг 1 4 . 2 1 . Листинг 1 4. 2 1 . Перемещение камеры в ответ н а нажатия кнопок

1 bool my_game : : Processinput ( ) 2 { З DIDEVICEOBJECTDATA keyboardData [КEYВOARD_BUFFER_S IZE ] ; 4 DIDEVICEOBJECTDATA mouseData [MOUSE_BUFFER_SIZE] ; НRESULT resul t ; 5 DWORD totalElements ; 6 7 DWORD i ; 8 totalElements = КEYBOARD_BUFFER_SIZE ; 9 result = theApp . Keyboard ( ) ->GetDeviceData ( 10 sizeof (DIDEVICEOBJECTDATA) , 11

41 1

Гото в и мся создават ь игры 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60

keyboardData , &totalElements , О) ; i f ( result ! = D I_OK) resul t = theApp . Keyboard ( ) ->Acquire ( ) ; while ( result DIERR_INPUTLOST) { result = theApp . Keyboard ( ) - >Acquire ( ) ; ==

/ * Если приоритет принадлежит другому приложеНИJО или произошла еще какая-то ошибка . . . * / if ( ( result - D IERR_OTHERAPPНASPRIO) 1 1 ( result DIERR_NOTACQUIRED ) ) ==

11 Просто попробуем еще раз позже . return ( true ) ;

else bool cameraМoved = false ; for ( i=O ; iAcquire ( ) ; while (result D IERR_INPUTLOST) { result = theApp . Mouse ( ) - >Acquire ( ) ; ==

/* Если приоритет принадлежит другому приложению или произошла еще какая-то ошибка . . . * / if ( (result -- DIERR_OTНERAPPНASPRIO) 1 1 ( result == DIERR_NOTACQUIRED ) ) // Просто попробуем еще раз позже . return ( true ) ;

return ( true ) ;

В листинге 1 4 . 2 1 приведен полный текст метода Process input ( ) класса игры. В этой версии изменяются положения точки, на которую направлена камера, и точки, в которой эта камера находится. Как это де­ лается, видно в строках 4 7-49, 59-6 1 , 7 1 - 7 3 и 73-85 . Переменные loo­ kAtPoint, eyePoint и cameraМoved - это рrivаtе-элементы класса игры, как видно из листинга 1 4 . 2 2 . Листинг 1 4 .22. Класс игры с подвижной камерой

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

clas s my_game : puЫ ic game { private : / / Геометрия и окружапцая обстановка . D3DXVECTORЗ eyePoint ; D3DXVECTOR3 lookatPoint ; D3DXVECTOR3 upDirection ; ground theGround ; puЫic : / / Методы , используемые платформой . bool OnAppLoad () ; bool PreD3Dinitialization ( ) ; bool PostD3Dinitial i zation ( } ; bool Gameinitialization ( ) ; bool HandleМe ssage (

Глава 1 4

41 4 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 } ;

НWND hWnd, UINT msg , WPARAМ wParam , LPARAМ l Param) ; Ьооl Proce ss inpu t ( ) ; Ьооl InvalidateDeviceObj ects ( ) ; Ьооl RestoreDeviceOb j ects ( ) ; bool UpdateFrame ( ) ; bool RenderFrame ( ) ; bool GameCleanup ( ) ; 1 1 Методы , используемые при инициализации . void SetupinputDevices ( ) ; void SetupViewМatrix ( ) ; void SetupProjectionМatrix ( ) ; void SetupGeome try ( ) ;

Поскольку переменные, управляющие положением камеры, теперь включены в класс игры, игра может изменять положение камеры в лю­ бой момент. Именно это и делается в методе Processinput ( ) из листин­ га 1 4 . 2 1 . Если пользователь нажимает клавиши перемещения курсора (стрелки вверх, вниз, влево или вправо), метод Processinput ( ) изменяет значения переменных, отслеживающих положение камеры. В строках 92-99 листинга 1 4 . 2 1 метод Processinput ( ) передает DirectЗD новое по­ ложение камеры . Он вызывает функцию DЗDxмatrixLookAtLH ( ) , чтобы создать новую матрицу отображения, и передает созданную матрицу классу dЗd арр. Когда выполняется следующее обновление кадра, плат­ форма авто�атически обнаруживает, что матрица отображения измени­ лась, и передает DirectЗD новую матрицу. Это делается точно так же, как и передача начальных матриц отображения в программах примеров. Если вы захотите посмотреть на код программы с подвижной каме­ рой, то сможете найти его на прилагающемся к книге компакт-диске (в папке Source \Chapter14 \Camera).

И то г и В этой главе мы кардинально переработали платформу физического мо­ делирования. Эти изменения было необходимо сделать, чтобы платформу можно было использовать для создания реалистичных игр. На прилагаю­ щемся к книге компакт-диске обновленная платформа находится в папке Source\Chapterl 4 \ Framework.

Гла ва 1 5

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

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

М ощность. сила, ускорение и трен ие По историческим причинам мы обычно измеряем выходную мощность автомобильных двигателей в лошадиных силах (horse power - hp) . Одна лошадиная сила - это примерно О. 75 киловатта. Для игр эти сведения не слишком полезны. Гораздо удобнее забыть о мощности и учитывать силу. Мощность и сила - это разные вещи. Мощность определяет общее ко­ личество работы, которую выполняет сила или вращающий момент за единицу времени. Если мы будем работать непосредственно с силами и моментами, а не с мощностями, мы упростим нашу задачу. Поршни в цилиндрах двигателей вращают коленчатый вал, который, в свою очередь, вращает карданный вал. Другими словами, двигатель развивает некоторое усилие, которое через цепочку передач прикладыва­ ется к ведущей оси (или осям, если у автомобиля больше двух ведущих колес). Разумеется, на каждом колесе есть шина. При вращении колеса вращающий момент шины прикладывает усилие к дороге. Это иллюстри­ рует рисунок 1 5 . 1 .

Глава 1 5

41 6

Рис. 1 5 . 1 . Сил ы , действующие на ведущее колесо

р

Собственно говоря, вы уже видели этот рисунок раньше. Это повторе­ ние рисунка 1 3 . 7 из главы 1 3 « Вода и волны >) . Как видно из рисунка, шина прикладывает усилие к дороге, и дорога прикладывает направлен­ ное в обратную сторону усилие к шине. Это третий закон Ньютона. Вся­ кое действие рождает равное противодействие. Сила взаимодействия между шиной и дорогой определяется трением. Как уже говорилось в главе 1 3 , шина, которая не скользит по дороге, ис­ пытывает трение качения. Если усилие, прикладываемое к ведущему ко­ лесу, недостаточно для преодоления трения качения, колесо будет прикладывать к дороге силу, и сила, противодействующая этой силе, бу­ дет толкать машину вперед. Величина этой силы определяется вращаю­ щим моментом колеса. Ее можно найти по формуле: F=

Т r

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

Автомоби л и , корабл и и л одки

417

Здесь Fs это сила трения качения, µ 8 - коэффициент трения качения, а N нормальная сила. В случае автомобиля нормальная сила - это сила тяжести F = mg. Поэтому мы можем записать: -

-

Fs ::;; µsmg Это уже формула, которую можно использовать в программе. Если ваша игра моделирует движение автомобиля, то можно воспользоваться методами для опроса клавиатуры и джойстика, описанными в главе 1 4 « Готовимся создавать игры >) . Если пользователь нажимает стрелку вверх или наклоняет вперед джойстик, программа увеличивает усилие на веду­ щих колесах. Если это усилие превышает µ 8mg, колесо должно начать проскальзывать. При этом трение между шиной и землей должно вычис­ ляться по формуле:

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

Пр едупреждение Делать игры настолько реалистичн ы м и необязательно. В подавляющем большинстве случаев в этом нет необходимости. Следует выдерживать ба­ ланс между реализмом и быстродействием . Не забывайте, что вам нужно обеспечи вать частоту обновления кадров - не меньше 30 кадров в секунду.

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

Гл ава 1 5

418

В более сложных программах, использующих Direct3D, можно ис­ пользовать другой способ. Программа вращает и перемещает сетчатую модель автомобиля . Проще говоря, она хранит результаты поворотов и перемещений в вертексном буфере. Если программе нужно найти точ­ ку соприкосновения колеса с грунтом , она просто считывает ее из вер­ тексного буфера. Если выключить двигатель автомобиля и перевести передачу на ней­ траль, то движущийся автомобиль постепенно остановится (если только он не катится вниз по склону). Причин остановки две. Первая - сопро­ тивление воздуха (мы рассмотрим его ниже). Вторая - трение между ко­ лесами автомобиля и другими его компонентами. Если нам нужно точно моделировать автомобиль , мы должны моделировать и это трение. Одна­ ко чаще всего это слишком сложная вычислительная задача для игр. Так как же смоделировать трение движущихся частей автомобиля, не просчитывая трения? Нужно имитировать трение. Создавая класс автомобиля в программе, добавьте в него коэффициент трения, который мы будем использовать для этой имитации. Обозначим его µCF· Этот коэффициент будет участво­ вать в следующей формуле:

vy = (1 - µcy)vc

Здесь vF есть скорость в конце заданного временного интервала, а vc - ско­ рость в начале этого интервала. Значение µCF должно лежать в пределах от О . О до 1 . 0 . Согласно этой формуле скорость автомобиля будет уменьшать­ ся с каждым кадром . Хотя эта формула и не выведена из физических за­ конов, результат ее использования будет вполне реалистичным.

С о п роти влен и е воздух а движущи м ся автомобилям Сопротивление воздуха в играх моделируется несколькими способами. Если вы хотите, чтобы моделирование было точным с точки зрения физи­ ки, нужно находить силу сопротивления воздуха по формулам для вязко­ го трения из главы 1 3 . Для удобства эти формулы приведены ниже в таблице 1 5 . 1 .

Таблица

1 5 . 1 . Выч исление силы сопротивления воздуха

Фор мула

Описани е

Fpn = -CFDv

Эту ф ормулу можно использовать, если автомобиль движется недостаточно быстро, чтобы вызвать турбулентность воздуха

2 Fpn = -Cynv

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

41 9

Автомоби л и , кора бл и и л одки

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

vy = (1 - µлу)vс

В этой формуле µ AF - коэффициент трения, связанного с сопротивле­ нием воздуха. Как и значение µ CF • его значение должно лежать в преде­ лах от О . О до 1 . 0 .

Пр едупреждение Не используйте такое упрощение при моделировании быстро движущихся автомобилей. Если вы пишете симулятор автомобильных гонок, вам при­ дется использовать формулы из табли цы 1 5 . 1 .

Торможение Торможение автомобиля - это обратная сторона разгона. Н а автомобиль в обоих случаях действуют одни и те же силы. Тормоза замедляют враще­ ние колес. Это приводит к появлению силы, противодействующей силе, прикладываемой колесами к грунту (см. рис . 1 5 . 2).

р

Рис. 1 5. 2 . Силы, действующие на колесо при торможении

Пока сила торможения, которую колесо прикладывает к дороге, мень­ ше силы трения качения, автомобиль будет плавно замедлять движение и

Глава 1 5

420

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

d = v2/(2g(µcos(