WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 || 3 | 4 |   ...   | 6 |

«Том Миллер Managed DirectX*9 Программирование графики и игр **омпэ*> Предисловие Боба Гейнса Менеджера проекта DirectX SDK корпорации Microsoft SAMS [Pi] KICK START Managed DirectX 9 ...»

-- [ Страница 2 ] --

} Это относительно очевидный алгоритм. В зависимости от того, какой тип примитива мы выбираем в нашем цикле, мы просто вызываем функ­ цию DrawPrimitives с соответствующим типом примитива. Обратите вни­ мание, что для набора точек мы просто используем число вершин в каче­ стве числа используемых точек. Описанные выше LineList и TriangleList отображают изолированные примитивы, которые для нашего обращения к DrawPrimitives с этими типами должны иметь число вершин, кратных числу вершин в отдельном примитиве (два или три, соответственно). Учитывая тот факт, что каждая последующая линия рисуется из после­ дней точки предыдущей линии, количество отображаемых линий для примитивов LineStrip должно быть на одну меньше, чем текущее число вершин. Для примитивов TriangleStrip и TriangleFan это требование по­ хоже, с поправкой на то, что число треугольников должно быть на два меньше, чем общее число вершин. Выполнение данного приложения отобразит сначала последователь­ ность точек на экране, а затем соответствующие линии. Затем эти строки соединятся в сплошную ломаную линию, а далее в набор изолированных треугольников. В конечном счете, на экране отобразится полоса или, в другом случае, веер из треугольников. Обратите внимание, размеры то­ чек можно изменять (значение «scale» в установках состояния рендера). Добавьте следующую строку к вашей функции SetupCamera для того, чтобы увеличить размер каждой точки в три раза:

Часть I. Введение в компьютерную графику device.RenderState.PointSize = 3.0 f ;

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

Использование индексных буферов Если вспомнить наше первое приложение, где мы рисовали куб и со­ здавали данные для 36 вершин, мы имели 2 треугольника для каждой из шести граней куба;

таким образом, мы имели 12 примитивов. Поскольку каждый примитив имеет 3 вершины, общее число вершин примитивов равно 36. Однако, в действительности вершин было только 8, для каждо­ го из углов куба. Сохранение данных одних и тех же вершин для приведенного приме­ ра может показаться не таким убедительным, но в больших приложени­ ях, где вы имеете огромное количество данных, было бы весьма полез­ ным не сохранять многократно одинаковые вершины, экономя при этом место в памяти. К счастью, Direct3D имеет механизм для решения таких задач, называемый «индексным буфером». Исходя из названия, индексный буфер — это буфер для записи индек­ сов в данные вершин. Индексы, сохраненные в этом буфере, могут быть 32-разрядными (целочисленные данные) или 16-разрядными (короткие данные). При небольшом количестве индексов можно использовать 16разрядный формат. При использовании индексного буфера для отображения примитивов каждый индекс в буфере соответствует конкретной вершине и ее дан­ ным. Например, треугольник с индексами 0, 1, 6 отобразился бы в соот­ ветствии с данными, сохраненными в этих вершинах. Давайте изменим наше приложение, рисующее куб, используя при этом индексы. Сначала изменим функцию создания данных вершин, как показано в листинге 4.3:

Листинг 4.3. Создание вершин для нашего куба. vb = new VertexBuffer(typeof(CustomVertex.PositionColored), 8, device, Usage.Dynamic | Usage.WriteOnly, CustomVertex.PositionColored.Format, Pool.Default);

CustomVertex.PositionColored[] verts = new CustomVertex.PositionColored[8];

// Vertices verts[0] = new CustomVertex.PositionColored(-1.0f, l.Of, l.Of, Color. Purple. ToArgb());

Глава 4. Более совершенные технологии рендеринга verts[1] = n w CustomVertex.PositionColored(-1.0f, -l.Of, 1.Of, e Color. Red. ToArgb());

verts[2] = new CustomVertex.PositionColored(1.0f, l.Of, l.Of, Color.Blue.ToArgb());

verts[3] = new CustomVertex.PositionColored(1.0f, -l.Of, l.Of, Color.Yellow.ToArgb());

verts[4] = new CustomVertex.PositionColored(-1.0f, l.Of, -l.Of, Color.Gold.ToArgb());

verts[5] = new CustomVertex.PositionColored(1.0f, l.Of, -l.Of, Color.Green.ToArgb());

verts[6] = new CustomVertex.PositionColored(-1.0f, -l.Of, -l.Of, Color.Black.ToArgb());

verts[7] = new CustomVertex.PositionColored(l.Of,-l.Of,-l.Of, Color.WhiteSmoke.ToArgb());

buffer.SetData(verts, 0, LockFlags.None);

Как можно увидеть, мы значительно уменьшили количество вершин, оставив только те 8, которые составляют углы куба. Мы можем нарисо­ вать 36 вершин, задавая различные параметры для каждого набора из 8 вершин. Но, исходя из предыдущего приложения, мы можем найти каж­ дую из используемых 36 вершин и соответствующий ей индекс в нашем новом списке. Добавьте список индексов, приведенный в листинге 4.4, к разделу определения данных:

Листинг 4.4. Данные индексного буфера для создания куба. private static readonly short[] indices = { 0,1,2, // Front Face 1,3,2, // Front Face 4,5,6, // Back Face 6,5,7, // Back Face 0,5,4, // Top Face 0,2,5, // Top Face 1,6,7, // Bottom Face 1,7,3, // Bottom Face 0,6,1, // Left Face 4,6,0, // Left Face 2,3,7, // Right Face 5,2,7 // Right Face };

Для простоты чтения индексный список разбивается на три — для каждого треугольника, которогое мы будем рисовать. Ясно, что лицевая сторона куба создается из двух треугольников. Первый треугольник ис­ пользует вершину О, 1, 2, в то время как второй треугольник использует Часть I. Введение в компьютерную графику вершину 1, 3, 2. Точно так же для правой грани куба первый треугольник использует вершину 2, 3, 7, второй треугольник использует 5, 2, 7. Пра­ вила отбора невидимой поверхности остаются в силе и при использова­ нии индексов. Тем не менее, имея только список индексов, мы вряд ли сможем чтото сделать, не изменив наше приложение. Для этого необходимо создать индексный буфер. Добавьте следующую строку после объявления вер­ шинного буфера:

private IndexBuffer ib = null;

Этот объект будет использоваться и для хранения индексов, и для обес­ печения доступа приложения Direct3D к этим индексам. Данная проце­ дура напоминает использование вершинного буфера, который мы уже создали, только вместо данных о вершине IndexBuffer содержит индек­ сы. Рассмотрим примеры использования данного объекта, заполнив его данными. После создания вершинного буфера добавьте код, приведен­ ный в листинге 4.5:

Листинг 4.5. Создание индексного буфера. ib = new IndexBuffer(typeof(short), indices.Length,device,Usage.WriteOnly,Pool.Default);

ib.Created += new EventHandler(this.OnlndexBufferCreate);

OnlndexBufferCreate (ib, null);

private void OnlndexBufferCreate(object sender, EventArgs e) IndexBuffer buffer = (IndexBuffer) sender;

buffer.SetData (indices, 0, LockFlags.None);

} Обратите внимание, что конструктор для индексного буфера напоми­ нает конструктор для вершинного буфера. Единственное различие — ог­ раничения на параметры типа. Как упоминалось выше, можно использо­ вать как короткие данные (System.Intl6), так и целочисленные значения (System.Int32) данных. При использовании индексного буфера мы также вызываем обработчик события и функцию обработчика прерываний для первого запуска. Затем мы просто заполняем наш индексный буфер не­ обходимыми данными. Теперь, чтобы использовать введенные данные, необходимо только вставить код рендеринга. Если вы помните, была функция, именуемая SetStreamSource, которая сообщала приложению Direct3D, какой вершин­ ный буфер будет использоваться при выполнении рендеринга. Существует похожая функция и для индексных буферов, однако в этот раз, она ис Глава 4. Более совершенные технологии рендеринга пользуется просто как признак, поскольку одновременно использоваться может только один тип индексного буфера. Установите этот признак сра­ зу после вызова функции SetStreamSource:

device.Indices = ib;

Теперь, когда Direct3D «знает» о нашем индексном буфере, мы долж­ ны изменить вызов рисунка. В данном случае мы пытаемся отобразить 12 примитивов (36 вершин) из нашего вершинного буфера, который ес­ тественно будет работать некорректно, поскольку он включает данные только о восьми вершинах. Следует вернуться назад и добавить функ­ цию DrawBox:

private void DrawBox(float yaw, float pitch, float roil, float x, float y, float z) ( angle += O.Olf;

device.Transform.World = Matrix.RotationYawPitchRolI(yaw, pitch, roll) * Matrix.Translation(x, y, z);

device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 8, 0, indices.Length / 3);

Итак, мы изменили вызов процедуры создания рисунка DrawPrimitives на DrawIndexedPrimitives. Рассмотрим прототип этой функции:

public void DrawIndexedPrimitives ( Microsoft.DirectX.Direct3D.PrimitiveType primitiveType, System.Int32 baseVertex, System.Int32 minVertexIndex, System.Int numVertices, System.Int32 startlndex, System.Int32 primCount ) Первый параметр такой же, как и в предыдущей функции, — тип при­ митивов, которые мы собираемся рисовать. Параметр BaseVertex — сме­ щение от начала индексного буфера до первого индекса вершины. Пара­ метр MinVertexIndex — минимальный индекс вершины в этом вызове. Параметр NumVertices — значение очевидно (число вершин, используе­ мых в течение этого вызова), однако он запускается вместе с параметра­ ми baseVertex и minVertexIndex. Параметр Startlndex определяет место­ положение в массиве для запуска считывания данных о вершине. После­ дний параметр остается тем же самым (число отображаемых примити­ вов). Таким образом, видно, что мы пытаемся нарисовать 8 вершин, исполь­ зуя индексный буфер, для того чтобы отобразить 12 примитивов для на­ шего куба. Теперь давайте удалим функции DrawPrimitives и заменим их Часть I. Введение в компьютерную графику следующими строками для нашего метода DrawBox, приведенного в ли­ стинге 4.6:

Листинг 4.6. Рисование кубов. // Draw our boxes DrawBoxfangle / (float)Math.PI, (float)Math.PI / 4.Of, O.Of, O.Of, O.Of);

DrawBoxfangle / (float(Math.PI, (float)Math.PI * 4.Of, 5.Of, O.Of, O.Of);

DrawBoxfangle / (float)Math.PI, (float)Math.PI / 2.Of, -5.Of, O.Of, O.Of);

DrawBoxfangle / (float(Math.PI, (float)Math.PI / 4.Of, O.Of, -5.Of, O.Of);

DrawBoxfangle / (float)Math.PI, (float)Math.PI * 4.Of, 5.Of, -5.Of, O.Of);

DrawBoxfangle / (float)Math.PI, (float)Math.PI / 2.Of, -5.Of, -5.Of, O.Of);

DrawBoxfangle / (float)Math.PI, (float)Math.PI / 4.Of, O.Of, 5.Of, O.Of);

angle / (float)Math.PI * 2.Of, angle / angle / (float)Math.PI / 2.Of, angle / angle / (float)Math.PI * 4.Of, angle / angle / (float)Math.PI * 2.Of, angle / angle / (float)Math.PI / 2.Of, angle / angle / (float)Math.PI * 4.Of, angle / angle / (float)Math.PI * 2.Of, angle / DrawBoxfangle / (float)Math.PI, angle / (float)Math.PI / 2.Of, angle / (float)Math.PI * 4.Of, 5.Of, 5.Of, O.Of);

DrawBoxfangle / (float)Math.PI, angle / (float)Math.PI * 4.Of, angle / (float)Math.PI / 2.Of, -5.Of, 5.Of, O.Of);

При выполнении данное приложение отобразит на экране очень кра­ сочные вращающиеся кубы. Причиной, по которой каждая вершина в нашем списке имеет разные цвета, является необходимость визуально показать один из недостатков использования индексных буферов при одновременном использовании вершин несколькими примитивами. Когда вершина разделена «между» несколькими примитивами, все данные вершин разделены, включая цве­ та и данные нормали. Таким образом, вы можете либо действительно одновременно использовать эту вершину, либо вам необходимо сначала определить, может ли одновременное использование этих данных вызы Глава 4. Более совершенные технологии рендеринга вать ошибки при освещении объекта (поскольку при вычислении осве­ щения используются данные нормалей к поверхностям). Вы можете так же видеть, что каждый угол куба представлен цветом его вершины, а гра­ ни куба интерполированы из цветов его вершин.

Использование буферов глубины или Z-буферов Буфер глубины, depth buffer (часто упоминаемый как Z-буфер или Wбуфер), используется приложением Direct3D для хранения информации о глубине отображаемого объекта. Эта информация используется в про­ цессе растеризации, чтобы определить, насколько пиксели перекрывают друг друга. На данном этапе, наше приложение не имеет буфера глуби­ ны, поэтому пиксели изображения не перекрываются в течение всего про­ цесса растеризации. Попробуем нарисовать еще несколько кубов, кото­ рые будут накладываться на некоторые из имеющихся. Добавьте следую­ щие строки в конце наших существующих обращений к DrawBox:

DrawBox(angle / (float)Math.PI, angle / (float)Math.PI * 2.Of, angle / (float)Math.PI / 4.Of, O.Of, (float)Math.Cos(angle), (float)Math.Sin(angle));

DrawBox(angle / (float)Math.PI, angle / (float)Math.PI / 2.Of, angle / (float)Math.PI * 4.Of, 5.Of, (float)Math.Sin(angle), (float)Math.Cos(angle));

DrawBox(angle / (float)Math.PI, angle / (float)Math.PI / 2.Of, angle / (float)Math.PI / 2.Of, -5.Of, (float)Math.Cos(angle), (float)Math.Sin(angle) );

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

public Microsoft.DirectX.Direct3D.DepthFormat AutoDepthStencilFormat [get, set] public bool EnableAutoDepthStencil [get, set] Часть I. Введение в компьютерную графику ОПТИМИЗАЦИЯ ПАМЯТИ ПРИ ИСПОЛЬЗОВАНИИ ИНДЕКСНЫХ БУФЕРОВ Для того чтобы определить, насколько использование индексного буфера позволяет экономить память для нашего приложения, срав­ ним относительно простые приложения отображения куба с исполь­ зованием индексного буфера и без него. В первом сценарии мы создавали вершинный буфер, включающий в себя 32 вершины типа CustomVertex.PositionColored. Эта структура содержала 16 байт (4 байта на каждый параметр X, Y, Z и цвет). Можно умножить этот размер на число вершин, получается, что наши данные вершин за­ нимают 576 байт. Теперь сравним это с алгоритмом, использующим индексный бу­ фер. Мы используем только 8 вершин (того же самого типа), так что наш размер данных вершин составляет 128 байтов. Однако, мы так­ же должны хранить наши индексные данные, при этом мы исполь­ зуем короткие индексы (2 байта на каждый), 36 индексов. Таким образом, наши индексные данные занимают 72 байта, суммируя это значение с 128 байтами, мы имеем полный размер 200 байтов. Сравните это с нашим первоначальным размером 576 байтов. Мы сократили требуемую память на 65 %. Экстраполируя эти значения на очень большие сцены, нетрудно представить, насколько эффек­ тивным может быть использование индексных буферов в плане эко­ номии памяти.

Установка EnableAutoDepthStencil в значение «true» включает буфер глубины для устройства, используя необходимый формат глубины, ука­ занный в параметре AutoDepthStencilFormat. Применимые значения фор­ матов глубины DepthFormat перечислены в таблице 4.1: Таблица 4.1. Возможные форматы Z буферов Формат D16 D32 D16Lockable D32FLockable D15S1 Описание 16-разрядный Z-буфер 32-разрядный Z-буфер 16-разрядный Z-буфер с возможностью блокировки 32-разрядный Z-буфер. Блокируемый формат. Использует стандарт ШЕЕ с плавающей запятой 16-разрядный Z-буфер, использует 15 бит на канал глубины, с последним битом, используемым для шаблонного (stencil) канала (такие каналы будут обсуждаться позднее) Глава 4. Более совершенные технологии рендеринга Формат D24S8 D24X8 D24X4S4 Описание 32-разрядный Z-буфер. Использует 24 бита для канала глубины и 8 бит для шаблонного канала 32-разрядный Z-буфер. Использует 24 бита для канала глубины, оставшиеся 8 бит игнорируются 32-разрядный Z-буфер. Использует 24 бита для канала глубины, с 4 битами, используемыми для шаблонного канала. Оставшиеся 4 бита игнорируются 32-разрядный Z-буфер. Использует 24 бита для канала глубины (с плавающей запятой) и 8 битов для шаблонного канала D24FS ВЫБОР СООТВЕТСТВУЮЩЕГО БУФЕРА ГЛУБИНЫ На сегодняшний день практически любая приобретаемая графичес­ кая плата поддерживает Z-буфер;

однако, в зависимости от обсто­ ятельств, при использовании этого буфера могут возникнуть про­ блемы. При вычислении глубины пиксела приложение Direct3D раз­ мещает пиксель в определенном диапазоне Z-буфера (обычно от O.Of до 1.Of), но это размещение редко является равномерным по всему диапазону. Отношение передней и задней плоскости напря­ мую определяет картину распределения в Z-буфере. Например, если ваша передняя плоскость определяется значени­ ем 1.Of, а задняя плоскость значением 100.Of, то 90 % всего диапа­ зона будут использоваться в первых 10 % вашего буфера глубины. В предыдущем примере, если бы задняя плоскость определялась значением 1000.Of, то 98 % всего диапазона использовалось бы в первых 2 % буфера глубины. Это могло бы привести к появлению «артефактов» при отображении отдаленных объектов. Использование другого буфера глубины — W-буфера — устраняет эту проблему, но имеет свои собственные недостатки. При исполь­ зовании W-буфера возможно появление «артефактов» скорее для ближних объектов, нежели для отдаленных. Также следует отметить, что не так много графических карт поддерживают W-буферы, по сравнению с Z-буферами. И последнее, для увеличения эффективности процедуры рендерин­ га при использовании буферов глубины лучше отображать элемен­ ты от передней плоскости (самое большое значение Z) к задней (са­ мое маленькое значение Z). В течение растеризации сцены Direct3D может быстро отбросить пиксель, который уже перекрыт, и отобра­ зить видимое поле рисунка полностью. Это не относиться к случаю, когда мы проводим рендеринг данных с алфавитными компонента­ ми, но к этому вопросу мы вернемся позже.

Часть I. Введение в компьютерную графику Буферы глубины большего размера могут хранить гораздо большее количество данных глубины, но ценой производительности и скорости. Если в этом нет необходимости, используйте буферы с меньшим разме­ ром. Современные графические платы поддерживают как правило 16разрядные буферы глубины, поэтому мы можем добавить это значение к существующей структуре параметров:

presentParams.EnableAutoDepthStencil = true;

presentParams.AutoDepthStencilFormat = DepthFormat.D16;

Замечательно, теперь в нашем устройстве имеется буфер глубины. Остается опробовать работу нашего приложения с новым буфером и срав­ нить результаты. Запускаем приложение. К сожалению, что-то нарушилось в выполнении приложения, и мы не видим того, чего ожидали. Что же случилось с нашими кубами? Почему добавление буфера глубины к нашему устройству вызвало сбой в работе программы? Это достаточно интересно: оказывается, наш буфер глуби­ ны ни разу не был «очищен» или сброшен, что и вызвало ошибку в рабо­ те. Попытаемся сделать это. Измените обращение к функции clear следу­ ющим образом:

device.Clear(ClearFlags.Target [ ClearFlags.ZBuffer, Color.CornflowerBlue, l.Of, 0);

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

Краткие выводы В этой главе мы охватили более совершенные методы рендеринга, включая. Использование индексных буферов, позволяющих обойти лиш­ нее дублирование вершин и таким образом увеличить эффектив­ ность обработки и отображения данных. • Различные типы примитивов, принципы создания каждого из них. • Буферы глубины, их описание и использование. В следующей главе мы будем отображать на экране реальные объек­ ты, загруженные из файлов. Кроме того, пришло время переходить к объектам «mesh».

Глава 5. Рендеринг «Mesh» Глава 5. Рендеринг Mesh-объектов В течение этой главы мы рассмотрим следующие пункты. Применение объектов Mesh. • Использование материалов, загрузка материалов. • Рисование общих объектов с использованием Mesh-технологии. • Использование Mesh для загрузки и рендеринга внешних файлов.

Определение Mesh-объектов В процессе построения и отображения объектов, помимо создания вершин вручную, ввода индексных данных, возможна также загрузка данных вершин из внешнего источника, например файла. Обычный фор­ мат таких файлов —.X файл. В предыдущих главах мы создавали только простые объекты, которые отображались на экране. Определение дан­ ных для треугольника и куба не составило огромного труда для нас. Но если представить объект, у которого имеются десятки тысяч вершин, вместо 36, которых мы использовали, усилия для написания такого кода или процедуры были бы более чем значительными. К счастью, в Управляемом DirectX есть объект, который может инкап­ сулировать сохраненные и загружаемые вершины, а также индексиро­ вать данные. Будем называть такой объект — объект Mesh. Объекты Mesh могут использоваться для сохранения любого типа графических данных, но главным образом предназначены для формирования сложных моде­ лей. Технология Mesh включает несколько методов или алгоритмов, по­ зволяющих увеличить эффективность рендеринга отображаемых объек­ тов. Все Mesh-объекты будут содержать вершинный буфер и индексный буфер (с которыми мы уже ознакомились), плюс буфер атрибутов, кото­ рый мы рассмотрим позже в этой главе. Фактический объект Mesh постоянно находится в библиотеке расши­ рений (Direct3D Extensions library (D3DX)). До сих пор мы имели только ссылки на главное приложение Direct3D, поэтому, прежде чем использо­ вать Mesh-объекты для нашего проекта, мы должны добавить ссылку на библиотеку Microsoft.DirectX.Direct3DX.dll. Далее, используя объект Mesh, попробуем создать наше приложение для вращающегося куба. После того как мы загрузили необходимые ссылки в наш проект, необ­ ходимо объявить переменную для объекта Mesh. Ее можно разместить там же, где размещались бы значения вершинного или индексного буфера:

private Mesh mesh = null;

Имеются три метода для создания объектов Mesh;

однако, сейчас нам не понадобится ни один из них. Есть несколько статических методов клас Часть I. Введение в компьютерную графику са Mesh, которые мы можем использовать при создании или загрузке раз­ личных моделей. Один из первых методов, на который следует обратить внимание, — метод «Box». Судя по названию, данный метод создает объект Mesh, включающий в себя «куб». Для рассмотрения и примене­ ния данного метода добавим следующую строку сразу после кода созда­ ния устройства: mesh = Mesh.Box(device, 2.Of, 2.Of, 2.0f);

Данный метод создаст новый Mesh-объект, который содержит верши­ ну и индексы, необходимые для рендеринга куба с заданной высотой, шириной и глубиной (значение 2.Of). Это куб того же размера, что и со­ зданный нами раньше вручную, используя вершинный буфер. Невероят­ но, но мы уменьшили количество строк этой процедуры до одной. Теперь, когда мы создали Mesh-объект, будут ли наши действия теми же, что и раньше, или нам необходимы новые методы? Ранее, при ото­ бражении нашего куба мы вызывали функцию SetStreamSource, чтобы сообщить приложению Direct3D, какой вершинный буфер используется для считывания данных, а также определяли индексы и свойства форма­ та вершин. При использовании Mesh-объектов отпадает необходимость в использовании указанных действий. РИСУНОК, ИСПОЛЬЗУЮЩИЙ MESH-ОБЪЕКТ Уточним некоторые вопросы относительно вершинного буфера для Mesh-объектов. При использовании объектов Mesh сохраняются вер­ шинный буфер, индексный буфер и формат вершин. Когда Meshобъект подвергается рендерингу, он автоматически устанавливает потоковый источник, также как индексы и свойства формата вершин. Теперь, когда наш Mesh-объект создан, необходимо отобразить его на экране. Все Mesh-объекты разбиты на группу подмножеств (на основе буфера атрибутов, который мы обсудим вскоре), а также имеется метод DrawSubset, который мы можем использовать для нашего рендеринга. Перепишем функцию DrawBox следующим образом:

private void DrawBox(float yaw, float pitch, float roll, float x, float y, float z) { angle += O.Olf;

device.Transform.World = Matrix.RotationYawPitchRoil(yaw, pitch, roll) * Matrix.Translation(x, y, z);

mesh.DrawSubset(0);

i Глава 5. Рендеринг «Mesh» Как можно видеть, мы заменили наш вызов DrawIndexedPrimitives на вызов DrawSubset. Стандартные примитивы, создаваемые классом Mesh (такие как Mesh.Box), будут всегда иметь единственное подмно­ жество. Это пока все, что мы должны были сделать для работы нашего прило­ жения. Удивительно просто, не правда ли? Теперь опробуем его. Итак, мы опять получили наши девять вращающихся кубов, но, увы, они все бесцветные. Если посмотреть на вершинный формат, созданный для объекта Mesh (через свойства VertexFormat), можно увидеть, что здесь списаны только данные нормалей и местоположение объекта. Цвета объекта Mesh не были определены и, поскольку мы выключили подсвет­ ку, кубы остались неосвещенными и неокрашенными. Если вы помните, в главе 1 («Введение в Direct3D») говорилось о том, что освещение работает только тогда, когда имеются данные о нормалях, сохраненные для вершин, и поскольку у нас имеются некоторые данные о нормалях для нашего куба, попробуем включить только подсветку фона. Освещение в устройстве установлено по умолчанию, поэтому вы можете либо удалить строку, определяя значение «false», либо установить соот­ ветствующее значение «true». Итак, мы успешно превратили наши неокрашенные кубы в черные. Но к сожалению, мы не имеем никакого освещения самой сцены. Пора ознакомиться с термином «общее освещение» или «общий свет» и ис­ пользовать данную характеристику в нашей сцене. Общий свет обеспечивается постоянным источником света для сце­ ны. При этом все объекты в сцене будут освещены равномерно, посколь­ ку общее освещение не зависит ни от каких коэффициентов (например, местоположения источника, направленности, ослабления и пр.), в отли­ чие от других методов освещения. Вам даже не понадобятся данные о нормалях для задания общего света. Но, несмотря на свою эффектив­ ность, такой свет не позволяет добиться реалистичности картинки. Тем не менее, пока мы воспользуемся преимуществами общего освещения. Добавьте следующую строку в раздел, где описывалось состояние рен­ дера подсветки:

device.RenderState.Ambient = Color.Red;

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

Часть I. Введение в компьютерную графику Использование материалов и освещения Итак, чем же отличается нынешнее освещение от того, которое мы уже использовали раньше? Единственное главное различие (кроме того, что используется Mesh) — недостаток цвета в наших данных вершины. Это и привело к неудаче в нашем случае. Для того чтобы Direct3D правильно вычислял цвет в отдельных точ­ ках на трехмерном объекте, не достаточно указать только цвет источника освещения, требуется указать и то, как объект будет отражать свет опре­ деленного цвета. В реальном мире, если вы освещаете красным светом синюю поверхность, на последней появится мягкий фиолетовый отте­ нок. Вам необходимо описать, как поверхность (в данном случае, наш куб) отражает свет от источника. В приложении Direct3D эту особенность описывают так называемые «материалы». Вы можете определить, каким образом объект будет отра­ жать общий диффузный свет, какие участки будут освещены больше или меньше (обсудим позже), и отражает ли объект свет вообще. Добавьте следующий код к вашему обращению DrawBox (до вызова DrawSubset):

Material boxMaterial = new Material!);

boxMaterial.Ambient = Color.White;

boxMaterial.Diffuse = Color.White;

device.Material = boxMaterial;

Здесь мы создаем новый материал и устанавливаем значения общего и диффузного освещения, выбрав белый цвет. Использование белого оз­ начает, что мы отразим весь общий и диффузный свет, падающий на объек­ ты. Затем мы используем свойства материалов в нашем устройстве для того, чтобы Direct3D «знал», какой материал использовать при ренде­ ринге. Запуская приложение, мы вновь видим на экране вращающиеся крас­ ные кубы. Изменение цвета общего освещения изменит и цвет каждого куба в сцене, а изменение цвета компонента материала изменит цвет от­ раженного от объекта света. Изменяя цвет материала, убирая из него крас­ ный цвет и имея общее освещение, мы уже никак не сможем получить обратно черные кубы, мы можем получать более темные полутона. Та­ ким образом, все определяется цветом освещения и выбранным матери­ алом. ИСПОЛЬЗОВАНИЕ БОЛЕЕ РЕАЛИСТИЧНОГО ОСВЕЩЕНИЯ Можно предположить, что представленные таким образом кубы не выглядят очень реалистичными. Вы не можете даже видеть цент Глава 5. Рендеринг «Меsh»-объектов ральные вершины и грани куба, объект выглядит сплошным и нере­ льефным. Оказывается, все зависит от того, как общий свет осве­ щает сцену. Если вы помните, при определении освещения отра­ женный свет вычислялся одинаково для всех вершин в сцене, неза­ висимо от нормали к плоскости объекта или любого другого пара­ метра освещения, рис.5.1.

Рис. 5. 1. Общее освещение без использования шейдинга (ретуширования) Чтобы отобразить наш куб более реалистично, мы должны добавить реальный свет к нашей сцене. Просмотрите строку, описывающую общий свет, и после этого добавьте следующие строки:

device.Lights[0].Туре = LightType.Directional;

device.Lights[0].Diffuse = Color.DarkBlue;

device.Lights[0].Direction = new Vector3(0, -1, - 1 ) ;

device.Lights[0].Commit ();

device.Lights[0].Enabled = true;

Это создаст источник направленного темно-синего света, направ­ ление которого совпадает с выбранным направлением камеры. Те­ перь приложение отобразит затененные вращающиеся темно-си­ ние кубы гораздо более реалистично. Можно увидеть, что направ­ ленные к наблюдателю грани освещены полностью, тогда как по­ вернутые грани кажутся более затененными (возможно даже пол­ ностью темными), рис.5.2.

Часть I. Введение в компьютерную графику Рис. 5.2. Затененные кубы с направленным освещением Имеются несколько готовых объектов, которые вы можете использо­ вать при использовании Mesh-файлов. Используйте любой из следую­ щих методов для создания этих объектов (каждая из готовых Mesh-фун­ кций требует в качестве первого параметра значение устройства): mesh = Mesh.Box(device, 2.Of, 2.Of, 2. Of);

Метод, использующий левую систему координат для создания куба.

Width - ширина Определяет размер куба вдоль оси X Height - высота Определяет размер куба вдоль оси Y Depth - глубина Определяет размер куба вдоль оси Z mesh = Mesh.Cylinder(device, 2.Of, 2.Of, 2.Of, 36, 36);

Метод, использующий левую систему координат для создания цилин­ дра.

Radiusl Радиус цилиндра должно быть больше Radius2 Радиус цилиндра должно быть больше на отрицательном конце оси Z. Это значение или равно O.Of. на положительном конце оси Z. Это значение или равно O.Of.

Глава 5. Рендеринг «Меsh»-объектов Length Длина цилиндра на оси Z. Slices Число секторов (slices) вдоль главной оси (большее значение добавит больше вершин). Stacks Число стеков вдоль главной оси (большее значение добавит больше вершин). mesh = Mesh.Polygon(device, 2.Of, 8);

Метод, использующий левую систему координат для создания поли­ гона.

Length Длина каждой стороны полигона. Sides Число сторон полигона. mesh = Mesh.Sphere(device, 2.Of, 36, 36);

Метод, использующий левую систему координат для создания сферы.

Radius Радиус сферы. Это значение должно быть больше или равно О Slices Число секторов (slices) вдоль главной оси (большее значение добавит больше вершин). Stacks Число стеков вдоль главной оси (большее значение добавит больше вершин). mesh = Mesh.Torus(device, 0.5f, 2.Of, 36, 18);

Метод, использующий левую систему координат для создания тора.

InnerRadius Внутренний радиус тора. Это значение должно быть больше или равно О OutterRadius Внешний радиус тора. Это значение должно быть больше или равно О Sides Число сторон в поперечном сечении тора. Это значение должно быть больше или равно трем. Rings Число колец в поперечном сечении тора. Это значение должно быть больше или равно трем. mesh = Mesh.Teapot(device) ;

Метод, использующий левую систему координат для создания изоб­ ражения заварочного чайника, рис.5.3.

- Зак. Часть I. Введение в компьютерную графику Рис. 5.3. Встроенный Mesh-обект в виде заварочного чайника Каждый из описанных методов также имеет вторую перегрузку опе­ раций, которая может возвращать информацию о смежных вершинах в виде трех целых чисел на каждую сторону объекта, которые определяют три «соседних» элемента каждой стороны Mesh-объекта.

Использование Mesh-объектов для рендеринга сложных моделей Отображение заварочных чайников выглядит вполне реалистично, но это не часто используется при программировании игр. Большинство Meshобьектов создано художниками, использующими приложения моделиро­ вания. Если ваше приложение моделирования поддерживает экспорт фай­ лов формата.X, вам повезло! ПРЕОБРАЗОВАНИЕ ОБЩИХ ФОРМАТОВ МОДЕЛИРОВАНИЯ В ФАЙЛ.X ФОРМАТА DirectX SDK (включенный в CD диск) включает в себя несколько ути­ лит преобразования для наиболее популярных приложений моде­ лирования. Использование таких утилит позволяет вам легко пре­ образовывать, сохранять и использовать ваши высококачественные модели в ваших приложениях.

Глава 5. Рендеринг «Mesh"-объектов Существует несколько типов данных, сохраненных в обычном «.х» файле, который может быть загружен при создании Mesh-объектов. Сюда можно отнести вершинные и индексные данные, которые потребуются при выполнении модели. Каждое из Mesh-подмножеств будет иметь со­ ответствующий материал. Набор материалов может также содержать ин­ формацию о текстуре. Вы можете также получить файл шейдера — вы­ сокоуровневого языка программирования для построения теней (дословно — High Level Shader Language, HLSL), используемого с этим Meshобъектом при загрузке файла. Язык шейдеров HLSL — более совершен­ ный раздел, который мы позже рассмотрим более подробно. Помимо статических методов с использованием Mesh-объектов, кото­ рые позволили нам создавать наши «простые» типы примитивов, суще­ ствуют еще два основных статических Mesh-метода, которые могут ис­ пользоваться для загрузки внешних моделей. Они называются Mesh.FromFile (с использованием файла) и Mesh.FromStream (с исполь­ зованием потока). Эти методы по существу идентичны, только в потоко­ вом методе имеется большее количество перегрузок в зависимости от размера потока. Корневые перегрузки для каждого метода следующие:

public static Microsoft.DirectX.Direct3D.Mesh FromFile ( System.String filename, Microsoft.DirectX.Direct3D.MeshFlags options, Microsoft.DirectX.Direct3D.Device device, Microsoft.DirectX.Direct3D.GraphicsStream adjacency, out Microsoft.DirectX.Direct3D.ExtendedMaterial[] materials, Microsoft.DirectX.Direct3D.EffectInstance effects ) public static Microsoft.DirectX.Direct3D.Mesh FromStream ( System.10.Stream stream, System.Int32 readBytes, Microsoft.DirectX.Direct3D.MeshFlags options, Microsoft.DirectX.Direct3D.Device device, Microsoft.DirectX.Direct3D.GraphicsStream adjacency, out Microsoft.DirectX.Direct3D.ExtendedMaterial[] materials, Microsoft.DirectX.Direct3D.EffectInstance effects ) Первым параметром является тип источника данных (filename или stream), который мы будем использовать для загрузки Mesh-объекта. В случае загрузки из файла эта строка определяет имя загружаемого Meshфайла. В случае использования потока строка указывает на поток и на число байтов, которые мы хотим считать для данных. При желании счи­ тать весь поток можно просто не включать значение readBytes. Параметр MeshFlags управляет тем, где и как загружены данные. Этот параметр может быть представлен в виде поразрядной комбинации зна­ чений, см. таблицу 5.1.

Часть I. Введение в компьютерную графику Таблица 5.1. Значения параметра MeshFlags Параметр MeshFlags.DoNotClip MeshFlags.Dynamic MeshFlags.IbDynamic MeshFlags.IbManaged MeshFlags.IbSoftwareProcessing MeshFlags.IbSystemMem Значение Использует флаг Usage.DoNotClip для вершинного и индексных буферов Равнозначное использование IbDynamic и VbDynamic Использует Usage.Dynamic для индексных буферов Использует пул памяти Pool.Managed для индексных буферов Использует флаг Usage.SoftwareProcessing для индексных буферов Использует пул памяти Pool.SystemMemory для индексных буферов Использует флаг Usage.WriteOnly Для индексных буферов Использует Usage.Dynamic для вершинных буферов Использует пул памяти Pool.Managed для вершинных буферов Использует флаг Usage. SoftwareProcessing для вершинных буферов Использует пул памяти Pool.SystemMemory для вершинных буферов Использует флаг Usage.WriteOnly для вершинных буферов Равнозначное использование IbManaged и VbManaged Использование флага Usage.NPatches для индексных и вершинных буферов. При рендеринге Mesh объекта потребуется дополнительный улучшенный N-Patch Использует флаг Usage.Points для индексных и вершинных буферов MeshFlags.lbWriteOnly MeshFlags.VbDynamic MeshFlags. VbManaged MeshFlags.VbSoftwareProcessing MeshFlags. VbSystemMem MeshFlags. VbWriteOnly MeshFlags.Managed MeshFlags.Npatches MeshFlags.Points Глава 5. Рендеринг «Meshw-объектов Параметр MeshFlags.RtPatches MeshFlags.SoftwareProcessing Значение Использует флаг Usage.RtPatches для индексных и вершинных буферов Равнозначное использование IbSoftwareProcessing и VbSoftwareProcessing Равнозначное использование IbSystemMem и VbSystemMem MeshFlags.SystemMemory MeshFlags.Use32Bit Использует 32-разрядные индексы для индексного буфера. Пока возможно, но обычно не рекомендуется Использует только аппаратную обработку MeshFlags.UseHardwareOnly Следующий параметр device — устройство, которое мы будем исполь­ зовать для рендеринга Mesh-объекта. Этот параметр обязательный, по­ скольку ресурсы должны быть связаны с устройством. Параметр adjacency является внешним параметром и означает, что параметр будет локализован и попадет к вам после того, как функция закончит выполнение. Это возвратит информацию о смежных вершинах в виде трех целых чисел на поверхность объекта, которые определяют три «соседних» элемента каждой поверхности Mesh-объекта. ЧТЕНИЕ ИНФОРМАЦИИ О СМЕЖНЫХ ЗНАЧЕНИЯХ ОТ ВОЗВРАЩАЕМОГО ПАРАМЕТРА GRAPHICSSTREAM Информация о смежных значениях, возвращаемая вам процедурой создания Mesh-объекта, будет приходить из класса GraphicsStream. Вы можете получить локальную копию смежных данных с помощью • следующего кода: int[] adjency = adjBuffer.Read(typeaf(int), mesh.NumberFaces * 3);

Это приведет к созданию массива из трех целых чисел на каждую поверхность объекта, в котором сохранится информация о смеж­ ных данных, и к которой проще обращаться, чем непосредственно к классу GraphicsStream. Параметр расширения materials также является выходным парамет­ ром, который возвратит массив, включающий информацию о различных Часть I. Введение в компьютерную графику подмножествах Mesh. Класс ExtendedMaterial поддерживает как обыч­ ный Direct3D материал, так и строку «string», которая может использо­ ваться для загрузки текстур. Обычно этой строкой является имя файла или название ресурса текстуры. Однако, поскольку загрузка текстуры выполняется в виде приложения, это могут быть любые введенные пользо­ вателем строковые данные. Наконец, последний параметр Effectlnstance описывает файл шейдера HLSL и значения, которые будут использоваться для данного Mesh-объек­ та. Существуют множественные перегрузки для каждого из этих мето­ дов, которые используют различные комбинации этих параметров. Ста­ райтесь выбирать тот метод, который имеет только необходимую инфор­ мацию. Теперь, проанализировав необходимую информацию, мы готовы к тому, чтобы загружать и отображать Mesh-объекты. Это выглядит слегка пугающе вначале, но на самом деле, не представляет больших сложнос­ тей. Попробуем начать писать соответствующий код. Сначала мы должны удостовериться, что мы имеем переменные объек­ та, которые позволят сохранить наши материалы и текстуры для различ­ ных подмножеств Mesh. Добавьте следующие переменные после объяв­ ления Mesh-объекта:

private Material[] meshMaterials;

private Texture [] meshTextures;

Поскольку встречается много различных подмножеств объектов, необ­ ходимо сохранить как массив текстур, так и массив материалов, по одному для каждого подмножества. Давайте рассмотрим некоторый код, позволя­ ющий загрузить Mesh-объект (функция LoadMesh), см. листинг 5.1:

Листинг 5.1. Загрузка Mesh объекта из файла. private void LoadMesh(string file) { ExtendedMaterial[] mtrl;

// Load our mesh mesh = Mesh.FromFile(file, MeshFlags.Managed, device, out mtrl);

// If we have any materials, store them if ((mtrl != null) && (mtrl.Length > 0)) { meshMaterials = new Material[mtrl.Length];

meshTextures = new Texture[mtrl.Length];

// Store each material and texture for (int i = 0;

i < mtrl.Length;

i++) { Глава 5. Рендеринг «Mesh»-o6beKTOB meshMaterials[i] = mtrl[i].Material3D;

if ((mtrl[i].TextureFilename != null) && (mtrl[i].TextureFilename != string.Empty)) { // We have a texture, try to load it meshTextures[i] = TextureLoader.FromFile(device, @"..\..\" + mtrl[i].TextureFilename);

} } } Сначала мы объявляем наш массив ExtendedMaterial, в котором будет храниться информация о подмножествах Mesh-объекта. Затем мы про­ сто вызываем метод FromFile для загрузки объекта. На данном этапе мы не учитываем смежные или HLSL-параметры, поэтому используем пере­ грузку без них. После загрузки Mesh-объекта необходимо сохранить информацию о материале и текстурах для различных подмножеств. После того как мы удостоверимся в наличии последних, мы, наконец, можем распределить массивы материалов и текстур, использующих число подмножеств в ка­ честве размера. Затем мы свяжем каждое из наших значений массива ExtendedMaterial и сохраним материал в нашей локальной копии. Если имеется информация о текстуре, включенной в это подмножество мате­ риалов, то для создания текстуры мы используем функцию TextureLoader.FromFile. Эта функция определена только двумя параметрами, уст­ ройством и именем файла текстуры, и является более предпочтительной, чем выполнение с помощью функции System.Drawing.Bitmap, рассмот­ ренной раньше. Для рисования данного Mesh-объекта добавьте следующий метод к нашему приложению: private void DrawMesh(float yaw, float pitch, float roll, float x, float y, float z) { angle += O.Olf;

device.Transform.World = Matrix.RotationYawPitchRoll(yaw, pitch, roll) * Matrix.Translation(x, y, z);

for (int i = 0;

i < meshMaterials.Length;

i++) { device.Material = meshMaterials [i];

device.SetTexture(0, meshTextures[i]);

mesh.DrawSubset(i);

Часть I. Введение в компьютерную графику Если вы обратили внимание, мы сохранили ту же самую последова­ тельность написания кода, что и при использовании метода DrawBox. Далее, чтобы рисовать Mesh-объект, необходимо связать все материалы и выполнить следующее: 1. Установить сохраненный материал как материал для устройства. 2. Установить значение текстуры в устройстве для сохраненной тек­ стуры. Даже если не имеется никакой сохраненной текстуры, установите значением текстуры нулевое значение или пустой указатель. 3. Вызвать функцию пересылки DrawSubset в нашем идентификаторе подмножества. Замечательно, теперь мы имеем все, что необходимо для загрузки Meshобъекта и отображения его на экране. Исходный текст нашего Mesh-объек­ та «tiny.x» находится в прилагаемом CD диске, данный файл является тестовым для приложения DirectX SDK. Чтобы его запустить мы добави­ ли следующие строки после создания устройства:

// Load our mesh LoadMesh(@"..\..\tiny.x");

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

device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI / 4, this.Width / this.Height, l.Of, 10000.Of);

device.Transform.View = Matrix.LookAtLH(new Vector3(0,0, 580.Of), new Vector3(), new Vector3(0,1,0));

Как видно, мы увеличили длину задней плоскости и переместили ка­ меру достаточно далеко назад. Осталось вызвать функцию DrawMesh:

DrawMesh(angle / (float)Math.PI, angle / (float)Math.PI * 2.Of, angle / (float)Math.PI / 4.Of, O.Of, O.Of, O.Of);

Запуск и выполнение приложения отобразит на экране картинку, при­ веденную на рис.5.4.

Глава 5. Рендеринг «Mesh-объектов 1* Рис. 5.4 Рендеринг Mesh-объекта, загруженного из файла Итак, мы получили нечто более реалистичное, чем просто вращаю­ щиеся кубы или треугольники.

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

Часть I. Введение в компьютерную графику Глава 6. Использование Управляемого DirectX для программирования игр Выбор игры Несмотря на то, что мы не обсудили более совершенные возможности трехмерного программирования, у нас имеется достаточно информации для того, чтобы написать простую трехмерную игру. Прежде чем начать писать игру, было бы неплохо придумать план. Мы должны знать тип игры, которую мы будем писать, основные осо­ бенности и пр. Учитывая ограниченный набор разделов, которые мы ох­ ватили, мы не можем пока создавать что-то чрезмерно сложное. Вместо этого, мы напишем достаточно несложную программу для нашей игры. Одна из первых демонстрационных игр для MS DOS называлась «Ос­ лик», «Donkey», в ней пользователь управлял автомобилем, и цель игры состояла в том, чтобы объехать осликов, встречающихся на дороге. Игра или ее оформление представляются достаточно простыми для того, что­ бы воссоздать ее на данный момент. В данной главе будет рассмотрено программирование этой игры в трехмерной варианте, но без осликов. Мы назовем эту игру «Dodger». Итак, потратим немного времени на планирование и проектирование нашей игры. Что нам необходимо, и что мы хотим сделать? Очевидно, нам понадобится класс «Автомобиль» или «Саг», чтобы управлять на­ шим транспортным средством. Затем, было бы неплохо иметь класс, от­ вечающий за управление препятствиями, которые мы будем пробовать объезжать. Плюс, мы будем нуждаться в нашем основном классе — движ­ ке игры, который будет выполнять весь рендеринг и связывать все это вместе. Если вы попытаетесь создать коммерческий вариант игры, вам пона­ добится затратить много времени на разработку описания игры, что яв­ ляется достаточно серьезным документом, детализирующим огромное количество игровых концепций и особенностей. Основная задача этой книги состоит в том, чтобы охватить процесс программирования игр, а не шаги, которые должны привести к изданию, поэтому вопросы ком­ мерциализации мы опустим. Другой рекомендуемый документ, который обычно требуется перед началом работы по разработке — техническая спецификация (для крат­ кости, спецификация). Она включает детальный список всех классов, а также методы и свойства, которые эти классы осуществят. Этот доку­ мент может также содержать диаграммы UML, которые отображают от­ ношения между объектами.

Глава 6. Использование DirectX для программирования игр Цель спецификации состоит в том, чтобы изначально и основательно обдумать проект вашего приложения, прежде чем вы начнете писать ка­ кой-либо код или программу. Поскольку акцент в книге поставлен на программирование игр, непосредственно вопросы по спецификации дан­ ного проекта мы также опустим. Но на будущее рекоммендуется всегда продумывать спецификацию перед написанием любого кода.

Программирование игры Вы можете запустить приложение Visual Studio и создать новый про­ ект. Создайте новый проект С# Windows Application под именем «Dodger». Следует отметить, что заданное по умолчанию название формы — Forml. Замените каждое имя Forml на имя DodgerGame, которое будет являться названием класса и будет представлено соответствующим кодом в этой главе. Необходимо добавить ссылки на три сборки Управляемого DirectX, которые мы уже использовали в проектах раньше, и включить для них using-директиву. Перепишите конструктор следующим образом:

public DodgerGame() ( this.Size = new Size(800,600);

this.Text = "Dodger Game";

this.SetStyle(ControlStyles.AllPaintinglnWmPaint | ControlStyles.Opaque, true);

} Данная процедура установит размер окна 800x600, заголовок окна и стиль для корректного выполнения рендеринга. Затем необходимо изме­ нить точку входа, заменяя основной метод «Main» на приведенный в ли­ стинге 6.1. Листинг 6.1. Основная точка входа игры. static void Main() { using (DodgerGame frm = new DodgerGame!)) { // Show our form and initialize our graphics engine frm.Show();

frm.InitializeGraphics();

Application.Run(frm);

} } Часть I. Введение в компьютерную графику По существу, это тот же самый код, с помощью которого мы уже за­ пускали все примеры раньше. Мы создаем окно Windows, инициализи­ руем графику и затем запускаем форму, и таким образом, приложение. Тем не менее, в функции инициализации InitializeGraphics, где и будет запускаться программа, необходимо внести некоторые изменения. Добавь­ те к вашему приложению метод, приведенный в листинге 6.2.

Листинг 6.2. Инициализация Графических Компонентов. ///

/// We will initialize our graphics device here /// public void InitializeGraphics!) ( // Set our presentation parameters PresentParameters presentParams = new PresentParametersO;

presentParams.Windowed = true;

presentParams.SwapEffeet = SwapEffect.Discard;

presentParams.AutoDepthStencilFormat = DepthFormat.D16;

presentParams.EnableAutoDepthStencil = true;

// Store the default adapter int adapterOrdinal = Manager.Adapters.Default.Adapter;

CreateFlags flags = CreateFlags.SoftwareVertexProcessing;

// Check to see if we can use a pure hardware device Caps caps = Manager.GetDeviceCaps(adapterOrdinal, DeviceType.Hardware);

// Do we support hardware vertex processing? if (caps.DeviceCaps.SupportsHardwareTransformAndLight) // Replace the software vertex processing flags = CreateFlags.HardwareVertexProcessing;

// Do we support a pure device? if (caps.DeviceCaps.SupportsPureDevice) flags ] = CreateFlags.PureDevice;

// Create our device device = new Device(adapterOrdinal, DeviceType.Hardware, this, flags, presentParams) ;

// Hook the device reset event device.DeviceReset += new EventHandler(this.OnDeviceReset);

this.OnDeviceReset(device, null);

Сначала создается структура параметров представления, и аналогич­ но тому, как мы это уже делали, определяется буфер глубины. Затем со­ храняется порядковый номер адаптера (заданный по умолчанию) и флаж­ ки создания устройств, после чего по умолчанию устанавливаются пара­ метры обработки вершин.

Глава 6. Использование DirectX для программирования игр На сегодняшний день современные графические платы могут поддер­ живать обработку вершин за счет аппаратного ускорения. Зачем же тра­ тить процессорное время, когда можно «возложить» часть операций не­ посредственно на графическую карту, которая выполнит это значительно быстрее? Однако, вы можете не знать, действительно ли используемый адаптер поддерживает эти возможности. Об этом немного позже. Теперь мы можем узнавать о возможностях устройства еще до его со­ здания, а также определять флажки, необходимые при создании устрой­ ства. Как вы помните из главы 2, структура отображаемого списка воз­ можностей устройства огромна и разбита на различные подразделы. Под­ раздел, представляющий интерес на данный момент, — DeviceCaps, со­ держит описание свойств и возможностей для соответствующего драй­ вера. Когда вы хотите выяснить, поддерживается ли данная специфическая возможность или нет, вы можете просто проверить булево значение, от­ носящееся к этой возможности: если это значение «true», возможность поддерживается, в противном случае, нет. В первую очередь вы выясня­ ете, поддерживаются ли в данном устройстве аппаратные преобразова­ ния и освещение. Если да, вы можете создавать устройство с аппаратной обработкой вершин, добавив флаг hardware vertex processing к осталь­ ным. Затем необходимо выяснить, можете ли вы создавать реальное уст­ ройство (возможно только при наличии аппаратной обработки вершин);

если да, вы используете для флажков поразрядный оператор «OR» («или»), добавляя также эту возможность. Реальное аппаратное устройство — наи­ более эффективный тип устройства, которое вы можете создать, так что если эти опции доступны, необходимо использовать их. Затем вы создаете устройство, используя указанные флажки, в зави­ симости от возможностей ваших графических плат, вы можете использо­ вать реальное аппаратное устройство или же некую его разновидность. Если помните, при использовании вершинных буферов возникала необ­ ходимость отслеживания события сброса устройства, подобная же ситу­ ация присутствует и здесь. Всякий раз, когда устройство сбрасывается, желательно определить установки состояния устройства по умолчанию. Поэтому необходимо отслеживать событие сброса. Метод обработчика событий или обработчик событий (event handler method) приведен в лис­ тинге 6.3, добавьте его к вашему приложению.

Листинг 6.3. Обработчик события сброса устройства private void OnDeviceReset(object sender, EventArgs e) ( device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI / 4, this.Width / this.Height, l.Of, 1000.Of);

Часть I. Введение в компьютерную графику device.Transform.View = Matrix.LookAtLH(new Vector3(0.0f, 9.5f, 17.Of), new Vector3(), new Vector3 (0,1,0));

// Do we have enough support for lights? if ((device.DeviceCaps.VertexProcessingCaps.SupportsDirectionalLights) && ((unit)device.DeviceCaps.MaxActiveLights > 1)) ( // First light device.Lights[0].Type = LightType.Directional;

device.Lights[0].Diffuse = Color.White;

device.Lights[0].Direction = new Vector3(l, -1, - 1 ) ;

device.Lights[0]. Commit ();

device.Lights[0].Enabled = true;

// Second light device.Lights[1].Type = LightType.Directional;

device.Lights[l].Diffuse = Color.White;

device.Lights[1].Direction = new Vector3(-l, 1, - 1 ) ;

device. Lights [ 1 ]. Commit () ;

device.Lights[l].Enabled = true;

else ( // Hmm.. no light support, let's just use // ambient light device.RenderState.Ambient = Color.White;

Начало этой функции достаточно знакомо для нас. Устанавливается камера, определяется вид и преобразование проекции в устройстве. Для этой игры выбирается неподвижная камера, и эти параметры необходи­ мо устанавливать после каждого сброса устройства (все состояния уст­ ройства аннулируются при сбросе). Использование общего освещения не является предпочтительным, поскольку мы уже видели, что общий свет не совсем реалистичен, более подходящим был бы направленный свет. Однако, мы уже не можем про­ верить, поддерживает ли устройство данный тип освещения или нет, по­ скольку после создания устройства структура Properties, «возможности» или «свойства», уже не используется. Если устройство может поддержи­ вать источник или источники направленного освещения, мы будем ис­ пользовать их, в противном случае возможна установка общего света по умолчанию. Возможно, это будет не так реалистично, но это лучше, чем иметь неосвещенную сцену. Необходимо окончательно переписать метод OnPaint для того, чтобы запустить процедуру рендеринга. Добавьте следующую функцию:

Глава 6. Использование DirectX для программирования игр protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { device.Clear(ClearFlags.Target \ ClearFlags.ZBuffer, Color.Black, l.Of, 0);

device.BeginScene();

device.EndScene();

device.Present();

this.Invalidated ;

} ИСПОЛЬЗОВАНИЕ ТОЛЬКО НЕОБХОДИМОГО ОСВЕЩЕНИЯ Вместо того чтобы использовать подход «все или ничего» в плане выбора типа освещения, вы можете просто проводить «многоуров­ невую» проверку поддержки режимов освещения. В данном сцена­ рии сначала выясняется, поддерживается ли хотя бы один источник света, и если да, можно включить его. Затем проверяется наличие второго поддерживаемого источника света, и так далее. Это дает возможность иметь резервный вариант (один источник) даже для тех устройств, которые не поддерживают два источника света и бо­ лее. Данный пример не совсем подходит при использовании одно­ го источника направленного света, поскольку многоуровневая про­ верка не была выполнена. Хотя даже в этом случае это выглядело бы примерно так:

// Do we have enough support for lights? if ((device.DeviceCaps.VertexProcessingCaps.SupportsDirectionalLights) && ((unit)device.DeviceCaps.MaxActiveLights > 0)) ( II First light device.Lights[0].Type = LightType.Directional;

device.Lights[0].Diffuse = Color.White;

device.Lights[0].Direction = new Vector3(l, - 1, -1);

device.Lights[0].Commit();

device.Lights[0].Enabled = true;

if ((unit)device.DeviceCaps.MaxActiveLights > 1)) { // Second light device.Lights[l].Type = LightType. Directional;

device.Lights[l].Diffuse = Color.White;

device.Lights[1].Direction = new Vector3(-l, 1, - 1 ) ;

device.Lights[1].Commit () ;

device.Lights[1].Enabled = true;

} } Часть I. Введение в компьютерную графику Вернемся к методу OnPaint. Ничего нового, кроме установки черного цвета фона. Теперь мы можем приступить к созданию первого игрового объекта — «road». Исходный текст программы находится на CD диске, включая файл.X, который будет отображать данные объекта «road», по­ этому мы должны объявить переменные для объекта «road mesh»:

// Game private private private board mesh information Mesh roadMesh = null;

Material[] roadMaterials = null;

Texture[] roadTextures = null;

Мы будем также использовать разновидности функции загрузки Meshобъекта, которую мы описали в предыдущей главе. Главные различия здесь состоят в том, что это будет статический объект, поэтому потребу­ ется вызывать данные из более чем одного класса и просматривать ин­ формацию о всех материалах и текстурах, в отличии от переменных класса level, которые использовались прежде. Добавьте следующий текст к ва­ шему коду, см. листинг 6.4.

Листинг 6.4. Процедура загрузки Mesh-объекта. public static Mesh LoadMesh(Device device, string file, ref Material!] meshMaterials, ref Texture[] meshTextures) { ExtendedMaterial[] mtrl;

// Load our mesh Mesh tempMesh = Mesh.FromFile(file, MeshFlags.Managed, device, out mtrl);

// If we have any materials, store them if ((mtrl != null) && (mtrl.Length > 0)) { meshMaterials = new Material[mtrl.Length];

meshTextures = new Texture[mtrl.Length];

// Store each material and texture for (int i = 0;

i < mtrl.Length;

i++) ( meshMaterials [i] = mtrl[i].Material3D;

if ((mtrl[i].TextureFilename != null) && (mtrl[i].TextureFilename ! = string.Empty)) ( // We have a texture, try to load it meshTextures[i] = TextureLoader.FromFile(device, @"..\..\" + mtrl[i].TextureFilename);

} } Глава 6. Использование DirectX для программирования игр return tempMesh;

} Эта функция уже обсуждалась раньше, так что нет необходимости описывать ее еще раз. Мы используем эту функцию, чтобы загрузить обьект «road», и нам следует определить и вызвать ее в процедуре обра­ ботчика события сброса устройства, в конце которой можно добавить следующий код: // Create our road mesh roadMesh = LoadMesh(device, @"..\..\road.x", ref roadMaterials, ref roadTextures) ;

Удостоверьтесь в том, что вы скопировали объект файл текстуры в каталог с вашим исходным кодом. Этот код загрузит объект Mesh «road», включая текстуры, и сохранит текстуры, материалы и объект. Теперь, когда вам нужно отобразить на экране объект дороги больше чем один раз в кадре, необходимо создать функцию рендеринга. Добавьте следующую функцию к вашему коду:

private void DrawRoad(float x, float y, float z) ( device.Transform.World = Matrix.Translation(x, y, z);

for (int i = 0;

i < roadMaterials.Length;

i++) ( device.Material = roadMaterials[i];

device.SetTexture (0, roadTextures [i]) ;

roadMesh.DrawSubset(i);

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

// Constant values for the locations public const float RoadLocationLeft = 2.5f;

public const float RoadLocationRight = -2.5f;

private const float RoadSize = 100.Of;

private const float MaximumRoadSpeed = 250.Of;

private const float RoadSpeedlncrement = 0.5f;

// Depth locations of the two 'road' meshes we will draw private float RoadDepthO = O.Of;

private float RoadDepthl = -100.Of;

private float RoadSpeed = 30.Of;

Mesh-объект «road», используемый для создания дороги — достаточ­ но распространенный объект. Его длина составляет 100 единиц, а шири­ на — 10 единиц. Константа размера отражает фактическую длину доро­ ги, в то время как две константы местоположения отмечают расстояние от центральной линии линий обеих сторон дороги. Последние две кон­ станты предназначены для управления в процессе ведения игры. Макси­ мальная скорость перемещения дороги составляет 250 единиц в секунду, при увеличении скорости дискретность составляет половину единицы. Наконец, необходимо установить значение глубины двух дорожных секций. Для этого следует инициализировать первую секцию как ноль, а вторую секцию начать непосредственно с конца первой (обратите внима­ ние, что это значение равно размеру дороги). Итак, мы имеем основные переменные и константы, необходимые для рисования и перемещения дороги, и можно добавить вызов процедуры рисования. Поскольку мы хотим отобразить дорогу первой, добавьте два вызова DrawRoad после функции рендеринга сцены BeginScene:

// Draw the two cycling roads DrawRoad(O.Of, O.Of, RoadDepthO);

DrawRoad(O.Of, O.Of, RoadDepthl);

Запустив приложение, видим, что дорога отображается на экране, од­ нако асфальт дороги смотрится чрезвычайно пикселизованным, несплош­ ным. Причиной такой пикселизации является способ, через который Direct3D определяет цвет пиксела в представленной сцене. Когда один элемент текстуры — тексел — охватывает больше чем один пиксел на Глава 6. Использование DirectX для программирования игр экране, пикселы рассчитываются фильтром растяжения. Когда нескольто элементов текстуры перекрывают отдельный пиксел, они рассчитыва­ ются фильтром сжатия. Заданный по умолчанию фильтр растяжения и сжатия, называемый точечным фильтром (Point Filter), попросту исполь­ зует самый близкий элемент текстуры как цвет для соответствующего пиксела. Это и вызывает эффект пикселизации. Существуют различные способы для фильтрации текстур, однако, не каждое устройство может поддерживать их. Все, что вам действительно необходимо, это фильтр, который может интерполировать элементы тек­ стуры дороги, чтобы выполнить ее более гладко. К функции OnDeviceReset добавьте код, приведенный в листинге 6.5.

Листинг 6.5. Фильтрация текстуры. // Try to set up a texture minify filter, pick anisotropic first if (device.DeviceCaps.TextureFilterCaps.SupportsMinifyAnisotropic) { device.SamplerState[0].MinFilter = TextureFilter.Anisotropic;

} else if (device.DeviceCaps.TextureFilterCaps.SupportsMinifyLinear) { device.SamplerState[0].MinFilter = TextureFilter.Linear;

} // Do the same thing for magnify filter if (device.DeviceCaps.TextureFilterCaps.SupportsMagnifyAnisotropic) { device.SamplerState[0].MagFilter = TextureFilter.Anisotropic;

} else if (device.DeviceCaps.TextureFilterCaps.SupportsMagnifyLinear) { device.SamplerState[0].MagFilter = TextureFilter.Linear;

} Как вы можете здесь видеть, вначале выясняется, способно ли ваше устройство поддерживать анизотропную фильтрацию при растяжении или сжатии изображения. Если да, то вы можете использовать этот фильтр хтя обоих режимов. Если нет, то далее выясняется, поддерживает ли ус­ тройство линейную фильтрацию для этих же режимов. Если ни один из фильтров не поддерживается, вы ничего не сможете сделать с пикселизацией дороги. В случае если плата поддерживает один из этих методов фильтрации, приложение отобразит на экране более гладкую поверхность дороги. Теперь дорога находится в середине экрана, но еще не перемещается. Понадобится новый метод, использующийся для обновления состояния игры, который позволит выполнить перемещение дороги и отследить Часть I. Введение в компьютерную графику столкновение автомобиля с препятствием. Для этого необходимо выз­ вать эту функцию в разделе метода OnPaint (до вызова функции очистки Clear):

// Before this render, we should update any state OnFrameUpdate ();

Также необходимо добавить к приложению метод, приведенный в листинге 6.6.

Листинг 6.6. Метод обновления кадра. private void OnFrameUpdate () { // First, get the elapsed time elapsedTime = Utility.TimerjDirectXTimer.GetElapsedlime);

RoadDepthO += (RoadSpeed * elapsedTime);

RoadDepthl += (RoadSpeed * elapsedTime);

// Check to see if we need to cycle the road if (RoadDepthO > 75.Of) { RoadDepthO = RoadDepthl - 100.Of;

} if (RoadDepthl > 75.Of) { RoadDepthl = RoadDepthO - 100.Of;

} Данная программа будет содержать гораздо больше строк, чем сей­ час, прежде чем написание игры будет завершено, но пока все, что нам действительно необходимо, это — перемещение дороги. Игнорирование параметра elapsed time позволяет перемещать дорогу и затем удалять пройденные дорожные секции, размещая их в конце текущей дорожной секции. При этом необходимо определить «количество» пройденной до­ роги, умножив текущую дорожную скорость (измеряемую в единицах за секунду) на количество прошедшего времени (в секундах), таким обра­ зом мы получаем «количество» дороги на кадр. Также необходимо вклю­ чить ссылку на elapsedTime в вашей секции объявления переменных:

private float elapsedTime = O.Of;

Глава 6. Использование DirectX для программирования игр ПЕРЕМЕЩЕНИЕ ОБЪЕКТОВ В РЕАЛЬНОМ МАСШТАБЕ ВРЕМЕНИ Почему это так необходимо? Скажем, вы решили увеличивать при­ ращение величины дороги при постоянном значении для каждого кадра. На вашем компьютере это выполняется совершенно, так по­ чему это не работает точно также на других системах? Например, дорога перемещается на другом компьютере несколько медленнее, чем на вашем. Или дорога перемещается удивительно медленно, по сравнению с тем, что испытывает человек, едущий на машине. Причина кроется в том, что вы выполняете ваши вычисления, опи­ раясь на частоту смены кадров. Например, скажем, в вашей систе­ ме изображение сменяется со скоростью 60 кадров в секунду, и все вычисления опираются на это значение. Теперь возьмем машины, которые работают с частотой обновления кадра 40 кадров в секун­ ду, или более быстрые, например, 80 кадров в секунду;

естествен­ но мы получим различные результаты. Таким образом, возникает задача, чтобы ваша игра выполнялась неизменно и независимо от типа системы, поэтому при вычислениях нужно уходить от привяз­ ки к частоте смены кадров. Лучший способ для решения этой проблемы состоит в том, чтобы определить и привязать игровые перемещения и вычисления к не­ которой неизменной единице времени. Например, максимальная скорость нашей дороги определена как 250 единиц в секунду. Наша первая цель состояла бы в том, чтобы определить время, прошед­ шее с момента нашего последнего обновления. В версии.NET Runtime имеется встроенная утилита (встроенный таймер), которая может использоваться для определения текущего отсчета времени системы, но у которой есть свой недостаток, связанный главным образом с низкой разрешающей способностью таймера, составля­ ющей, как правило, 15 миллисекунд. Это приводит к тому, что при высокой скорости смены кадров (более 60 кадров в секунду) дви­ жения будут казаться прерывистыми. Версия DirectX SDK включает в себя класс, называемый DirectXTimer, который, если ваша машина позволяет это, использует таймер с высоким разрешением (обычно 1 миллисекунда). Если данный тай­ мер не доступен на вашей машине, то система вернется к встроен­ ному таймеру. Примеры в этой книге будут использовать данный таймер (DirectXTimer) как механизм определения времени. Он уже включает в себя код для таймера высокой точности, поэтому мы не будем изобретать это «колесо» дважды.

Часть I. Введение в компьютерную графику Добавление движущегося автомобиля в используемую сцену Теперь, когда вы отображаете на экране объект в виде перемещаю­ щейся дороги, необходимо добавить объект, с которым взаимодействует игрок — это автомобиль (в листинге программы — «саг»). Вы могли бы просто разместить переменные класса «саг», константы и код в основной класс — «Main class», в котором размещены и дорожные секции, но это выглядело бы не так модульно, как хотелось. Было бы предпочтительнее размещать автомобильный код отдельно, в его собственном классе, кото­ рый мы попробуем сейчас создать. В главном меню проекта откройте опцию Add New Item и выберите в качестве добавляемого новый класс, названный «саг». Проверьте, что вы включили операторы использования Управляемого DirectX в этом новом файле кода, созданном для вашего приложения. Что же должен делать класс «саг»? Как мы уже говорили, в процессе рендеринга автомобиль остается неподвижным относительно перемеща­ ющейся сцены, за исключением перемещений влево-вправо, когда он должен объезжать препятствия. С учетом вышесказанного, необходимо добавить следующий набор переменных в наш класс:

// Car constants public const float Height = 2.5f;

public const float Depth = 3.0f;

public const float Speedlncrement = 0.If;

private const float Scale = 0.85f;

// Car data information private float carLocation = DodgerGame.RoadLocationLeft;

private float carDiameter;

private float carSpeed = 10.Of;

private bool movingLeft = false;

private bool movingRight = false;

// Our car mesh information private Mesh carMesh = null;

private Material[] carMaterials = null;

private Texture[] carlextures = null;

Данные переменные будут управлять всеми параметрами, необходи­ мыми для управления автомобилем. Константы высоты «Height» и глу­ бины «Depth» автомобиля останутся статическими (поскольку переме­ щение осуществляется только влево-вправо). Приращение скорости бо­ кового перемещения «Speedlncrement» также будет постоянным. После­ дняя константа «Scale» — масштаб или отношение размера автомобиля относительно ширины дороги.

Глава 6. Использование DirectX для программирования игр Переменные в классе «саr» очевидны. Текущее местоположение авто­ мобиля на дороге, которое установлено по умолчанию с левой стороны дороги. Диаметр автомобиля, который будет использоваться при столк­ новении автомобиля с препятствием. Текущая боковая скорость автомо­ биля (так как скорость перемещения дороги может увеличиваться, ско­ рость перемещения автомобиля должна увеличиться соответственно). И, наконец, две логические переменные, которые определяют направление перемещения (влево-вправо), а также файл.X данных объекта Mesh. Для создания объекта Mesh (и его связанных структур) и вычисления диаметра автомобиля потребуется конструктор класса саr. Замените за­ данный по умолчанию конструктор на специально созданный для ваше­ го приложения, см. листинг 6.7.

Листинг 6.7. Создание класса саr ///

/// Create a new car device, and load the mesh data for it /// /// D3D device to use public Car(Device device) { // Create our car mesh carMesh = DodgerGame.LoadMesh(device, @"..\..\car.x", ref carMaterials, ref carTextures);

// We need to calculate a bounding sphere for our car VertexBuffer vb = carMesh.VertexBuffer;

try { // We need to lock the entire buffer to calculate this GraphicsStream stm = vb.Lock(0, 0, LockFlags.None) ;

Vector3 center;

// We won't use the center, but it's required float radius = Geometry.ComputeBoundingSphere(stm, carMesh.NumberVertices, carMesh.VertexFormat, out center);

// All we care about is the diameter. Store that carDiameter = (radius * 2) * Scale;

} finally { // No matter what, make sure we unlock and dispose this vertex // buffer. vb.Unlock();

vb.Disposed;

} } Часть I. Введение в компьютерную графику Создание объекта Mesh достаточно просто, так как представляет собой тот же самый метод, который мы использовали при создании Mesh-объекта road, только с другими именами переменных. Новым алгоритмом здесь яв­ ляется только вычисление диаметра автомобиля. Вычисляется граничная сфера (сфера, которая полностью заключает в себе все точки в Mesh объек­ те) для автомобиля. Класс geometry содержит данную функцию, но она тре­ бует пересылки параметров вершин для вычисления граничной сферы. То, что нам сейчас необходимо, это получить вершинные данные из Mesh-объекта. Уже известно, что данные вершин хранятся в вершинных буферах, так что мы можем использовать вершинный буфер, сохранен­ ный в Mesh-объекте. Чтобы считать данные из вершинного буфера необ­ ходимо вызвать метод блокировки на момент считывания, Lock method, который возвращает все потоковые вершинные данные. О других мето­ дах, работающих с вершинными буферами, мы расскажем в следующей главе. После метода блокировки мы можем использовать метод ComputeBoundingSphere, чтобы получить «центр» этого Mesh-объекта и радиус сферы. Поскольку мы не заботимся о центре объекта и хотим со­ хранить диаметр, удвоим радиус и сохраним его. Окончательно (в блоке finally), мы проверяем, что вершинный буфер, который мы использова­ ли, разблокирован и освобожден. Далее необходимо добавить метод, выполняющий прорисовку ваше­ го автомобиля. Поскольку вы сохраняете положение автомобиля в клас­ се, единственная необходимая вещь, которая понадобится для этого ме­ тода — это используемое для рисунка устройство. Этот метод аналоги­ чен методу DrawRoad, отличаясь лишь в использовании переменных и в том, что мы масштабируем объект перед его выводом на экран. Добавьте следующий код:

///

/// Render the car given the current properties /// /// The device used to render the car public void Draw(Device device) { // The car is a little bit too big, scale it down device.Transform.World = Matrix.Scaling(Scale, Scale, Scale) * Matrix.Translation(carLocation, Height, Depth);

for (int i = 0;

i < carMaterials.Length;

i++) { device.Material = carMaterials[i];

device.SetTexture(0, carTextures[i]);

carMesh.DrawSubset(i);

} } Глава 6. Использование DirectX для программирования игр Перед тем как использовать класс саr, необходимо создать локальные переменные, к которым может понадобиться общий доступ. Добавьте этот список параметров к вашему классу саr:

// Public properties for car data public float Location get ( return carLocation;

} set ( carLocation = value;

} public float Diameter get ( return carDiameter;

) public float Speed get ( return carSpeed;

( set ( carSpeed = value;

1 public bool IsMovingLeft get ( return movingLeft;

} set ( movingLeft = value;

I public bool IsMovingRight get { return movingRight;

1 set ( movingRight = value;

} Далее необходимо добавить переменную для поддержки класса саг в основном движке игры. Добавьте следующие строки где-нибудь в разде­ ле определения переменных приложения DodgerGame: // Car private Car car = null;

Поскольку мы используем устройство в качестве параметра конструк­ тора, мы не можем создавать класс саr до момента создания этого устрой­ ства. Было бы неплохо поместить алгоритм создания автомобиля в методе OnDeviceReset. Для этого сразу после создания Mesh-объекта road добавь­ те следующие строки, чтобы создать объект саr в качестве устройства: // Create our car car = new Car (device);

Часть I. Введение в компьютерную графику После создания объекта класса саr мы можем изменить алгоритм рен­ деринга, чтобы запустить процедуру отображения автомобиля. Сразу после вызова двух методов DrawRoad из метода OnPaint добавьте вызов рисунка из объекта саr:

// Draw the current location of the car car.Draw(device);

Итак, мы отобразили автомобиль на перемещающейся дороге. Даль­ ше необходимо управлять движением автомобиля от одной стороны до­ роги к другой. Предположим, что пользователь имеет пока только клави­ атуру и для управления автомобилем будет использовать ее. Для этих целей наиболее подходят клавиши с изображением стрелок. Перепиши­ те метод OnKeyDown в классе DodgerGame, как указано в листинге 6.8.Листинг 6.8. Обработка команд с помощью клавиш. ///

/// Handle key strokes /// protected override void OnKeyDown(System.Windows.Forms.KeyEventArgs e) { // Handle the escape key for quiting if (e.KeyCode == Keys.Escape) { // Close the form and return this.Closed;

return;

} // Handle left and right keys if ((e.KeyCode == Keys.Left) || (e.KeyCode == Keys.NumPad4)) { car.IsMovingLeft = true;

car.IsMovingRight = false;

} if ((e.KeyCode == Keys.Right) || (e.KeyCode == Keys.NumPad6)) { car.IsMovingLeft = false;

car.IsMovingRight = true;

} II Stop moving if (e.KeyCode == Keys.NumPad5) { car.IsMovingLeft = false;

car.IsMovingRight = false;

} } Глава 6. Использование DirectX для программирования игр Ничего особенного здесь не происходит. При нажатии на клавишу «Escape» игра будет закончена закрытием формы. Нажатием левой или правой клавиши-стрелки мы записываем соответствующее значение «true» в переменной перемещения «влево», «вправо» (в распечатке IsMovingLeft и IsMovingRight), при этом для перемещения в противоположную сторо­ ну данная переменная устанавливается в значение «false». Перемещение автомобиля прекращается, если нажата клавиша «5» на цифровой клави­ атуре. Итак, при выполнении приложения нажатие этих клавиш заставит переменные изменять свои значения, но при этом сам автомобиль не бу­ дет двигаться. Необходимо также добавить функцию обновления для ав­ томобиля. Добавьте метод, приведенный в листинге 6.9, в ваш класс саг.

Листинг 6.9. Управление перемещением автомобиля. ///

/// Update the cars state based on the elapsed time /// /// Amount of time that has elapsed public void Update(float elapsedTime) ( if (movingLeft) { // Move the car carLocation += (carSpeed * elapsedTime);

// Is the car all the way to the left? if (carLocation >= DodgerGame.RoadLocationLeft) { movingLeft = false;

carLocation = DodgerGame.RoadLocationLeft;

} } if (movingRight) { // Move the car carLocation -= (carSpeed * elapsedTime);

// Is the car all the way to the right? if (carLocation <= DodgerGame.RoadLocationRight) { movingRight = false;

carLocation = DodgerGame.RoadLocationRight;

} } } Этот метод принимает в качестве параметра общее затраченное время «elapsed time» таким образом, чтобы мы могли поддерживать те же са Часть I. Введение в компьютерную графику мые перемещения на всех компьютерах. Сама функция достаточно про­ ста. Если одна из переменных перемещения принимает значение «true», мы будем двигаться в соответствующем направлении (опираясь на пара­ метр «elapsed time»). Затем проверяется местоположение автомобиля, если положение неправильное (автомобиль выходит за пределы дороги), дви­ жение останавливается полностью. Однако, в настоящий момент этот метод не вызывается, и для того чтобы его вызвать, необходимо изме­ нить метод OnFrameUpdate в классе DodgerGame. Добавьте следующую строку к концу этого метода:

// Now that the road has been 'moved', update our car if it's moving car.Update(elapsedTime);

Добавление препятствий Поздравляем! Это — первое интерактивное графическое ЗD-приложение, которое вы создали. Мы получили модель перемещения автомо­ биля или автомобильный симулятор. Несмотря на то, что на самом деле перемещается дорога (вниз относительно автомобиля), создается полное ощущение движения автомобиля. Таким образом, мы написали практи­ чески половину нашей игры. Теперь необходимо создать препятствия, которые вы потом будете объезжать. Подобно тому, как мы добавляли класс саг, необходимо добавить новый класс «Obstacle» («Препятствия»). Проверьте, что вы включили директиву «using» Управляемого DirectX в этом новом файле кода. Хотя предполагается использование одного Mesh-объекта для всех препятствий, было бы интересно внести некоторое разнообразие в спи­ сок препятствий, например, различную форму, тип или цвет препятствий. Можно использовать или готовую заготовку Mesh-объекта, изменяя его тип, или материалы для изменения цвета. Таким образом, следует доба­ вить константы и переменные, необходимые для класса obstacle, см. лис­ тинг 6.10.

Листинг 6.10. Константы класса «Obstacle». // Object constants private const int NumberMeshTypes = 5;

private const float ObjectLength = 3.Of;

private const float ObjectRadius = ObjectLength / 2.Of;

private const int ObjectStacksSlices = 18;

// obstacle colors private static readonly Color [] ObstacleColors = ( Color.Red, Color.Blue, Color.Green, Color.Bisque, Color.Cyan, Color.DarkKhaki, Глава 6. Использование DirectX для программирования игр Color.OldLace, Color.PowderBlue, Color.DarkTurquoise, Color.Azure, Color.Violet, Color.Tomato, Color.Yellow, Color.Purple, Color.AliceBlue, Color.Honeydew, Color.Crimson, Color.Firebrick };

// Mesh information private Mesh obstacleMesh = null;

private Material obstacleMaterial;

private Vector3 position;

private bool isleapot;

Как видно из первой константы, имеются пять различных типов Meshобъектов (сфера, куб, тор, цилиндр, и заварочный чайник, соответствен­ но, sphere, cube, torus, cylinder и teapot). Большинство этих типов будут иметь или параметр длины, или параметр радиуса, которыми можно ва­ рьировать для изменения размера препятствия. Существуют также до­ полнительные параметры Mesh-объекта (пачки, наборы, сектора, кольца и т.д), которые управляют числом полигонов (треугольники можно отне­ сти к простейшим полигонам), составляющих объект. За это отвечает последняя константа в списке — ObjectStacksSlices. При увеличении зна­ чения этой константы растет число используемых полигонов, а следова­ тельно, и качество картинки. Далее идет список цветов для Mesh-объекта. Мы беспорядочно выб­ рали несколько цветов и включили их в список. Обратите внимание, что мы не сохраняем в этом классе ни массив материалов, ни текстуры для наших объектов. Как известно, заданный по умолчанию тип Mesh содер­ жит только одно подмножество без текстур или материалов, поэтому эта дополнительная информация не нужна. Поскольку препятствия «лежат» на движущейся дороге, необходимо согласовать перемещение препятствия с мировой системой координат. Необходимо связывать положение препятствия, которое будет изменять­ ся с каждым кадром, с дорогой. Поэтому, поскольку мы не можем конт­ ролировать размер объекта «teapot», выбранного в качестве Mesh-объек­ та, в процессе его создания необходимо применить метод масштабирова­ ния. Замените конструктор класса obstacle на следующий:

public Obstacle(Device device, float x, float y, float z) { // Store our position position = new Vector3(x, y, z);

// It's not a teapot isTeapot = false;

// Create a new obstacle switch (Utility.Rnd.Next(NumberMeshTypes)) { 126 case 0:

Часть I. Введение в компьютерную графику obstacleMesh = Mesh.Sphere(device, ObjectRadius, ObjectStacksSlices, ObjectStacksSlices);

break;

case 1: obstacleMesh = Mesh.Box(device, ObjectLength, ObjectLength, ObjectLength);

break;

case 2: obstacleMesh = Mesh.Teapot(device) ;

isTeapot = true;

break;

case 3: obstacleMesh = Mesh.Cylinder(device, ObjectRadius, ObjectRadius, ObjectLength,ObjectStacksSlices, ObjectStacksSlices);

break;

case 4: obstacleMesh = Mesh.Torus(device, ObjectRadius / 3.0f, ObjectRadius / 2.Of, ObjectStacksSlices, ObjectStacksSlices) ;

break;

} // Set the obstacle color obstacleMaterial = new Material();

Color objColor = ObstacleColors[Utility.Rnd.Next(ObstacleColors.Length)];

obstacleMaterial.Ambient = objColor;

obstacleMaterial.Diffuse = objColor;

} Обратите внимание на использование здесь функции Rnd из модуля утилит. Исходник этой функции включен в CD диск. Ее задача заключа­ ется в том, чтобы просто возвращать случайные числа. Конструктор для нашего препятствия сохраняет заданное по умолчанию местоположение препятствия и значения по умолчанию для объекта Mesh. Затем он слу­ чайным образом выбирает один из типов Mesh-объекта и создает его. Наконец, он выбирает случайный цвет из списка и использует его как цвет материала для препятствий. Прежде чем включить препятствие в движок игры, необходимо учесть пару моментов. Сначала необходимо создать функцию, которая изменяет положение объекта в соответствии с положением дороги, для этого доба­ вим следующий код:

public void Update(float elapsedTime, float speed) { position.Z += (speed * elapsedTime);

} Глава 6. Использование DirectX для программирования игр Снова используется параметр «elapsed time», чтобы обеспечить и со­ гласовать различные скорости перемещений. Также пересылается теку­ щее значение скорости дороги, чтобы совместить перемещение объекта и дороги. Для отображения перемещения препятствия необходимо ис­ пользовать процедуру рендеринга. Добавьте метод, приведенный в лис­ тинге 6.11, к классу obstacle.

Листинг 6.11. Рисование препятствий. public void Draw(Device device) ( if (isTeapot) { device.Transform.World = Matrix.Scaling(ObjectRadius, ObjectRadius, ObjectRadius) * Matrix.Translation(position);

} else { device.Transform.World = Matrix.Translation(position);

} device.Material = obstacleMaterial;

device.SetTexturefO, null);

obstacleMesh.DrawSubset(O);

} Поскольку Mesh-объект «teapot» после создания не масштабируется (если вы рисуете один из этих объектов), необходимо сначала смасштабировать этот объект и уже затем переместить его в соответствующую позицию. Затем устанавливается материал для цвета объекта, присваива­ ется значение «null» для текстуры, и рисуется Mesh-объект. • Очевидно, потребуется иметь не одно препятствие на дороге. Поэто­ му нам понадобится простой метод добавления и удаления препятствий. Использование массива возможно, но нецелесообразно, поскольку нео­ днократное изменение размера массива несколько громоздко. Лучше со­ здать класс-коллекцию для хранения данных о препятствиях. Добавьте класс, приведенный в листинге 6.12, в конец вашего файла с кодом пре­ пятствия.

Листинг 6.12. Класс-коллекция Препятствий, класс obstacles. public class Obstacles : IEnumerable { private ArrayList obstacleList = new ArrayList();

Часть I. Введение в компьютерную графику ///

/// Indexer for this class /// public Obstacle this[int index] { get { return } // Get the enumerator from our arraylist public IEnumerator GetEnumeratorO { return obstacleList.GetEnumerator();

(Obstacle)obstacleList[index] ;

} ///

/// Add an obstacle to our list /// /// The obstacle to add public void Add(Obstacle obstacle) { obstacleList.Add(obstacle);

} ///

/// Remove an obstacle from our list /// /// The obstacle to remove public void Remove(Obstacle obstacle) { obstacleList.Remove(obstacle);

} ///

/// Clear the obstacle list /// public void Clear() { obstacleList.ClearO ;

} } Для правильной компиляции необходимо поместить директиву «using» для System.Collections в самом начале файла кода для этого класса. Этот класс имеет индексированный прямой доступ к объекту препятствия, а также метод перебора и следующие три действия: добавление, удаление и очистка, соответственно, add, remove и clear. Имея эти базовые возмож­ ности, можно приступить к добавлению объекта препятствия.

Глава 6. Использование DirectX для программирования игр Сначала необходимо внести переменную, которую можно использо­ вать для поддержки списка текущих препятствий в сцене. Добавьте сле­ дующую переменную в класс DodgerGame:

// Obstacle information private Obstacles obstacles;

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

///

/// Add a series of obstacles onto a road section /// /// Minimum depth of the obstacles private void AddObstacles(float minDepth) { // Add the right number of obstacles int numberToAdd = (int)((RoadSize / car.Diameter - 1) / 2.0f);

// Get the minimum space between obstacles in this section float minSize = ((RoadSize / numberToAdd) - car.Diameter) / 2.Of;

for (int i = 0;

i < numberToAdd;

i++) { // Get a random # in the min size range float depth = minDepth - ((float)Utility.Rnd.NextDouble() * minSize);

// Make sure it's in the right range depth -= (i * (car.Diameter * 2));

// Pick the left or right side of the road float location = (Utility.Rnd.Next (50) > 25) ?RoadLocationLeft:RoadLocationRight;

// Add this obstacle obstacles.Add(new Obstacle(device, location, ObstacleHeight, depth));

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

private const float ObstacleHeight = Car.Height * 0.S5f;

• IIK Часть I. Введение в компьютерную графику Осталось сделать три вещи перед тем, как препятствия появятся в сце­ не: необходимо добавить обращение к методу добавления препятствий, необходимо убедиться, что функция обновления вызывается для каждо­ го препятствия в сцене, и необходимо отобразить препятствия на экране. Кроме того, для сброса переменных при запуске новой игры необходимо создать соответствующую функцию и использовать данный алгоритм при первом вызове метода AddObstacles. Добавьте метод, приведенный в ли­ стинге 6.13.

Листинг 6.13. Загрузка игровых опций, заданных по умолчанию. ///

/// Here we will load all the default game options /// private void LoadDefaultGameOptions() { // Road information RoadDepthO = O.Of;

RoadDepthl = -100.Of;

RoadSpeed = 30.Of;

// Car data information car.Location = RoadLocationLeft;

car.Speed = 10.Of;

car.IsMovingLeft = false;

car.IsMovingRight = false;

// Remove any obstacles currently in the game foreach(Obstacle о in obstacles) { // Dispose it first o. Disposed;

} obstacles. Clear d;

// Add some obstacles AddObstacles(RoadDepthl);

// Start our timer Utility.Timer(DirectXTimer.Start);

} Этот метод принимает различные значения переменных, которые мож­ но при необходимости установить по умолчанию. Он также принимает любые существующие препятствия, находящиеся в списке, располагает их и очищает список перед заполнением новыми препятствиями. И нако­ нец, он запускает таймер, после чего необходимо добавить вызов создан­ ной функции после создания устройства в методе InitializeGraphics. He Глава 6. Использование DirectX для программирования игр добавляйте (!) эту функцию в метод OnDeviceReset;

мы хотим вызывать эту функцию, только когда начинается новая игра.

// Load the default game options LoadDefaultGameOptions () ;

Затем нужно добавить вызов в методе OnFrameUpdate для обнов­ ления препятствий при каждой смене кадра. Таким образом, перед ме­ тодом обновления объекта саr добавьте следующий код в метод OnFrameUpdate:

// Move our obstacles foreach(Obstacle о in obstacles) { // Update the obstacle, check to see if it hits the car o.Update(elapsedTime, RoadSpeed);

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

// Draw any obstacles currently visible foreach(Obstacle о in obstacles) { o.Draw(device);

} Теперь попытаемся запустить игру! Вы проехали часть дороги (есте­ ственно, перемещая саму дорогу), проехали несколько препятствий, что дальше? После этих нескольких препятствий больше ничего не появля­ ется. Вспомните, метод AddObstacles добавляет препятствия только к одной секции дороги! Таким образом, перемещая дорожные секции, мы не вызываем повторно метод создания препятствий для этих «новых» дорожных секций. Перепишите секцию кода, который используется для добавления новых дорожных секций, следующим образом:

// Check to see if we need to cycle the road if (RoadDepthO > 75.Of) { RoadDepthO = RoadDepthl - 100.Of;

AddObstacles(RoadDepthO);

} if (RoadDepthl > 75.Of) { Часть I. Введение в компьютерную графику RoadDepthl = RoadDepthO - 100.Of;

AddObstacles(RoadDepthl);

} Теперь это выглядят более реалистично. У нас есть автомобиль, пере­ мещающийся мимо препятствий (пока проносясь через препятствия). К самим препятствиям также можно добавить движение, например, заста­ вить их вращаться. Для этого вначале необходимо добавить несколько новых переменных к классу obstacle, чтобы управлять вращением пре­ пятствий:

// Rotation information private float rotation = 0;

private float rotationspeed = 0.Of;

private Vector3 rotationVector;

Скорость, с которой они вращаются, и оси вращения должны выби­ раться случайным образом. Это легко осуществить, добавляя следующие строки в конец конструктора класса obstacle:

rotationspeed = (float)Utility.Rnd.NextDouble() * (float)Math.PI;

rotationVector = new Vector3( (float)Utility.Rnd.NextDouble(), (float) Utility.Rnd.NextDouble(), (float) Utility.Rnd.NextDouble ()) ;

Осталось два момента, необходимых для того, чтобы задать правиль­ ное вращение препятствиям. Для начала необходимо включить враще­ ние в функцию обновления (update function) следующим образом:

rotation += (rotationspeed * elapsedTime);

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

if (isTeapot) { device.Transform.World = Matrix.RotationAxis(rotationVector, rotation) * Matrix.Scaling(ObjectRadius, ObjectRadius, ObjectRadius) * Matrix.Translation(position);

} else { device.Transform.World = Matrix.RotationAxis(rotationVector, rotation) * Matrix.Translation(position);

} Глава 6. Использование DirectX для программирования игр Теперь, при запуске игры, видны препятствия, беспорядочно враща­ ющиеся по мере увеличения или уменьшения скорости автомобиля. Ка­ ков следующий шаг? Необходимо добавить опции, позволяющие отсле­ живать состояние игры и текущий счет, который необходимо обнулять в начале новой игры и увеличивать после удачного прохождения препят­ ствия. Добавьте следующие значения переменных объекта к движку игры «main» в классе DodgerGame:

// Game private private private private information bool isGameOver = true;

int gameOverlick = 0;

booi hasGameStarted = false;

int score = 0;

Здесь сохраняется вся информация об игре. Вам необходимо знать, началась или закончилась игра, время окончания последней игры и теку­ щий счет. Было бы желательно учесть также скорость перемещения пре­ пятствий и увеличивать текущий счет в зависимости от сложности игры. Первое, что необходимо для этого сделать, — добавить следующие стро­ ки для сброса счета очков в опции LoadDefaultGameOptions, чтобы в на­ чале новой игры все состояния обнулялись: car.IsMovingRight = false;

score = 0;

В методе OnFrameUpdate до того момента, как вы начинаете переме­ щать препятствия, добавьте следующий код:

// Remove any obstacles that are past the car // Increase the score for each one, and also increase // the road speed to make the game harder. •Obstacles removeObstacles = new Obstacles));

foreach(Obstacle о in obstacles) { if (o.Depth > car.Diameter - (Car.Depth * 2)) ( // Add this obstacle to our list to remove removeObstacles.Add(o);

// Increase roadspeed RoadSpeed += RoadSpeedlncrement;

// Make sure the road speed stays below max if (RoadSpeed >= MaximumRoadSpeed) { RoadSpeed = MaximumRoadSpeed;

} // Increase the car speed as well Часть I. Введение в компьютерную графику car.IncrementSpeed();

// Add the new score score += (int)(RoadSpeed * (RoadSpeed / car.Speed));

} } // Remove the obstacles in the list foreach(Obstacle о in removeObstacles) { obstacles.Remove(o);

// May as well dispose it as well o.Disposed ;

} removeObstacles.Clear();

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

///

/// Increment the movement speed of the car /// public void IncrementSpeed() { carSpeed += Speedlncrement;

} Теперь следует добавить в класс obstacle новый метод для определе­ ния момента, когда автомобиль врезается в одно из препятствий:

public { // // // if { bool IsHittingCar(float carLocation, float carDiameter) In order for the obstacle to be hitting the car, it must be on the same side of the road and hitting the car (position.Z > (Car.Depth - (carDiameter / 2.0f))) // are we on the right side of the car if ((carLocation < 0) && (position.X < 0)) return true;

if ((carLocation > 0) && (position.X > 0)) return true;

Глава 6. Использование DirectX для программирования игр } return false;

} Довольно просто. Вы выясняете, если автомобиль находится в той же самой глубине секции и с той же самой стороны дороги, что и препят­ ствие, это означает столкновение с препятствием, и возвращается значе­ ние «true». В противном случае возвращается значение «false», автомо­ биль успешно преодолевает препятствие. Теперь необходимо вставить этот код в движок игры. Замените код обновления препятствия более со­ вершенным кодом:

// Move our obstacles foreach(Obstacle о in obstacles) ( // Update the obstacle, check to see if it hits the car o.Update(elapsedTime, RoadSpeed) ;

if (o.IsHittingCar(car.Location, car.Diameter)) { } } // If it does hit the car, the game is over. isGameOver = true;

gameOverTick = System.Environment.TickCount;

// Stop our timer Utility.Timer(DirectXTimer.Stop);

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

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

// Nothing to update if the game is over if ((isGameOver) || (!hasGameStarted)) return;

Часть I. Введение в компьютерную графику Теперь необходимо обработать нажатия клавиш, чтобы перезапустить игру. В конец перегрузки OnKeyDown Вы можете добавить следующую логику: if (isGameOver) { LoadDefaultGameOptions();

} // Always set isGameOver to false when a key is pressed isGameOver = false;

hasGameStarted = true;

Это и есть поведение, которого мы добивались. Теперь после оконча­ ния игры игрок нажимает клавишу, и новая игра начинается с заданных по умолчанию игровых опций. При желании вы можете удалить вызов опции LoadDefaultGameOptions из метода InitializeGraphics, поскольку она будет вызываться автоматически после нажатия клавиши. Однако, у нас пока нет кода, который вызовет небольшую паузу после прекращения игры. Вы можете поместить его в процедуру OnKeyDown или сразу пос­ ле проверки нажатия клавиши «Escape»:

// Ignore keystrokes for a second after the game is over if ((System.Environment.TickCount - gameOverTick) < 1000) { return;

} Выполнение данных строк игнорирует любые нажатия клавиши (кроме клавиши выхода Escape) в течение одной секунды после того, как игра за­ кончена. Теперь вы можете приступить к игре. Хотя можно еще добавить дополнительный текст, отображающий состояние игры или некоторые ком­ ментарии. В составе имен («namespace») приложения Direct3D имеется класс шрифтов («Font class»), который может использоваться для отображения тек­ ста, однако, Font class имеется и в пространстве имен System.Drawing. Эти два класса будут конфликтовать, если вы попытаетесь использовать «Font» без идентификации. К счастью, вы можете переименовать данный объект, используя директиву «using» следующим образом:

using Direct3D = Microsoft.DirectX.Direct3D;

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

Глава 6. Использование DirectX для программирования игр // Fonts private Direct3D.Font scoreFont = null;

private Direct3D.Font gameFont = null;

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

// Create our fonts scoreFont = new Direct3D.Font (device, new System.Drawing.Font("Arial", 12.Of, FontStyle.Bold));

gameFont = new Direct3D.Font(device, new System.Drawing.Font("Arial", 36.Of, FontStyle.Bold ! FontStyle.Italic));

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

if (hasGameStarted) { // Draw our score scoreFont.DrawText (null, string. Format ("Current score: (Of, score), new Rectangle(5,5,0,0), DrawTextFormat.NoClip, Color.Yellow) ;

} if (isGameOver) { // If the game is over, notify the player if (hasGameStarted) { gameFont.DrawText (null, "You crashed. The game is over.", new Rectangle(25,45,0,0), DrawTextFormat.NoClip, Color.Red);

} if ((System.Environment.TickCount - gameOverTick) >= 1000) { // Only draw this if the game has been over more than one second gameFont.DrawText(null, "Press any key to begin.", new Rectangle(25,100,0,0), DrawTextFormat.NoClip, Color.WhiteSmoke);

} } Часть 1. Введение в компьютерную графику Алгоритм отображения текста DrawText будет обсуждаться подроб­ нее в следующей главе. В отображаемых текстах можно показать любую информацию, например: текущий счет, сообщение о неудаче, сообщение о начале игры после нажатия любой клавиши и т.д. Итак, теперь у нас имеется законченный вариант игры. Вы можете запустить ее, фиксировать изменение счета, конец игры, неудачу, и затем начать игру сначала. Что осталось? Хорошо было бы сохранять лучший результат, который бы постоянно пересохранялся при более высоком ре­ зультате. ДОБАВЛЕНИЕ КОММЕНТАРИЯ «High Scores» — наилучший результат Нас в первую очередь будут интересовать имена нескольких игро­ ков и их максимальный результат. Создадим для этого простую структуру, добавив следующий код в главное пространство имен игры:

///

/// Structure used to maintain high scores /// public struct HighScore ( private int realScore;

private string playerName;

public int Score { get { return realScore;

} set ( realScore = value;

1 } public string Name { get { return playerName;

} set ( playerName = value;

) } I Дальше необходимо установить список результатов «high scores» в нашем движке игры. Мы рассмотрим формирование списка только для трех игроков, используя при этом массив для хранения этих данных. Добавьте следующие строки в движок игры:

// High score information private HighScore[] highScores = new HighScore[3] ;

private string defaultHighScoreName = string.Empty;

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

Глава 6. Использование DirectX для программирования игр ///

/// Check to see what the best high score is. If this beats it, /// store the index, and ask for a name /// private void CheckHighScore() { int index = -1;

for (int i = highScores.Length - 1;

i >= 0;

i—-) { if (score >= highScores[i].Score) // We beat this score { index = i;

} } II We beat the score if index is greater than 0 if (index >= 0) { for (int i = highScores.Length - 1;

i > index ;

i—-) { // Move each existing score down one highScores[i] = highScores [i-1];

} highScores[index].Score = score;

highScores[index].Name = Input.InputBox("You got a high score!!", "Please enter your name.", defaultHighScoreName);

} } ///

/// Load the high scores from the registry /// private void LoadHighScores() { Microsoft.Win32.RegistryKey key = Microsoft.Win32.Registry.LocalMachine.CreateSubKey( "Software\\MDXBoox\\Dodger") ;

try { for (int i = 0;

i < highScores.Length;

i++) { highScores[i].Name = (string)key.GetValue( string.Format("Player{0}", i), string.Empty);

highScores[i].Score = (int)key.GetValue( string.Format("Score(O)", i), 0);

} defaultHighScoreName = (string)key.GetValue( "PlayerName", System.Environment.UserName);

} Часть I. Введение в компьютерную графику finally { if (key != null) ( key.Closed;

// Make sure to close the key } } } ///

/// Save all the high score information to the registry /// public void SaveHighScores () { Microsoft.Win32.RegistryKey key = Microsoft.Win32.Registry.LocaiMachine.CreateSubKey( "Software\\MDXBoox\\Dodger");

try { for(int i = 0;

i < highScores.Length;

it++) { key.SetValue(string.Format("Player{0}", i ), highScores[i].Name);

key.SetValue(string.Format("Score{O}", i), highScores[i].Score);

} key.SetValue("PlayerName", defaultHighScoreName);

} finally { if (key != null) { key.Close();

// Make sure to close the key } } } He будем слишком глубоко вникать в конструкцию этих функций, по­ скольку они имеют дело главным образом со встроенными класса­ ми.NET и не воздействуют на код Управляемого DirectX. Однако, важно показать, откуда эти методы вызываются в движке игры. Проверка значения для «high scores» должна производиться сразу после окончания игры. Замените код в методе OnFrameUpdate, ко­ торый проверяет факт столкновения автомобиля с препятствием, на следующий: if (о.IsHittingCar(car.Location, car.Diameter)) { Глава 6. Использование DirectX для программирования игр // If it does hit the car, the game is over. isGameOver = true;

gameOverTick = System.Environment.TickCount;

// Stop our timer Utility.Timer(DirectXTimer.Stop);

// Check to see if we want to add this to our high scores list CheckHighScore();

} Вы можете загружать значение «high scores» в конце конструктора основного движка игры. Следует отметить, что алгоритм сохране­ ния является общим (тогда как другие методы являются индивиду­ альными). Поэтому мы будем вызывать его из нашего основного кода. Перепишите основной метод «main»: using (DodgerGame frm = new DodgerGame()) { // Show our form and initialize our graphics engine frm.Show();

frm.InitializeGraphics();

Application.Run(frm);

// Make sure to save the high scores frm.SaveHighScoresO ;

} И заключительный момент действия — отображение на экране списак наилучших результатов. Мы добавим его в наш код рендеринга. До того как вызвать окончательный метод для нашего текста, доба­ вим к секции кода рендеринга надписи «High scores» следующие строки: // Draw the high scores gameFont.DrawText(null, "High Scores: ", new Rectangle(25,155,0,0), DrawTextFormat.NoClip, Color.CornflowerBlue);

for (int i = 0;

i < highScores.Length;

i++) { gameFont.DrawText(null, string.Format("Player;

{0} : {1}", highScores[i].Name, highScores[i].Score), new Rectangle(25,210 + (i * 55),0,0), DrawTextFormat.NoClip, Color.CornflowerBlue);

} Поздравляем, вы только что закончили написание вашей первой игры.

Часть I. Введение в компьютерную графику Краткие выводы В этой главе мы проделали следующее. Использовали объекты Mesh для рендеринга игровых объектов. • Проверили некоторые возможности устройства. • Освоили простейший пользовательский ввод. • Создали систему подсчета очков. • Объединили все в общую конструкцию. Результаты игры приведены на рис. 6.1.

Рмс.б.1. Завершенная игра В следующей главе мы обсудим использование более совершенных возможностей и свойств mesh-объектов.

ЧАСТЬ II ОСНОВНЫЕ КОНЦЕПЦИИ ПОСТРОЕНИЯ ГРАФИКИ Глава 7. Использование дополнительных свойств и возможностей Mesh-объектов Глава 8. Введение в ресурсы Глава 9. Применение других типов Mesh Глава 10. Использование вспомогательных классов Часть II. Основные концепции построения графики Глава 7. Использование дополнительных свойств и возможностей Mesh-объектов В данной главе мы обсудим более широкие возможности применения объектов «Mesh», включая. • Оптимизацию данных Mesh-объекта. • Упрощение Mesh-объектов. • Создание Mesh с новыми компонентами данных о вершинах. • Объединение вершин.

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

Листинг 7.1. Добавление нормалей к Mesh-объекту. // Check if mesh doesn't include normal data if ((mesh.VertexFormat & VertexFormats.Normal) != VertexFormats.Normal) ( Mesh tempMesh = mesh.Clone(mesh.Options.Value, mesh.VertexFormat | VertexFormats.Normal, device);

tempMesh.ComputeNormals();

// Replace existing mesh mesh.Disposed ;

mesh = tempMesh;

} Здесь мы берем существующий Mesh-объект и определяем, содержит ли он данные нормалей или нет. Значение вершинного формата VertexFormat возвращает список параметров VertexFormats, которые объе­ динены через логический оператор «ИЛИ», поэтому мы используем опе­ ратор «И», чтобы определить, установлен ли бит нормали. В случае если он не установлен, мы создаем с помощью функции Clone второй, вре Глава 7. Использование свойств и возможностей Mesh-объектов менный Mesh-объект, являющийся копией первоначального объекта. Дан­ ный метод имеет три варианта загрузки:

public Microsoft.DirectX.Direct3D.Mesh Clone ( Microsoft.DirectX.Direct3D.MeshFlags options, Microsoft.DirectX.Direct3D.GraphicsStream declaration, Microsoft.DirectX.Direct3D.Device device ) public Microsoft.DirectX.Direct3D.Mesh Clone ( Microsoft.DirectX.Direct3D.MeshFlags options, Microsoft.DirectX.Direct3D.VertexEleraent[] declaration, Microsoft.DirectX.Direct3D.Device device ) public Microsoft.DirectX.Direct3D.Mesh Clone ( Microsoft.DirectX.Direct3D.MeshFlags options, Microsoft.DirectX.Direct3D.VertexFormats vertexFormat, Microsoft.DirectX.Direct3D.Device device ) В нашем примере мы использовали последнюю перегрузку. В каж­ дой из перегрузок, первые и последние параметры — те же самые. Па­ раметр опций — options позволяет вновь созданному Mesh-объекту иметь различный набор дополнительных опций. Вы можете сохранить те же самые опции в новом объекте-копии (то, что мы делали ранее), а можете изменить их. Было бы предпочтительнее, чтобы новый Meshобъект постоянно находился в системной памяти, нежели в управляе­ мой памяти. Любой из указателей MeshFlags, которые являются дос­ тупными при создании объекта, также доступны в течение операции создания копии. Последний параметр метода Clone device — устройство, на базе кото­ рого будет создан новый объект. В большинстве случаев это будет то же самое устройство, которое вы создаете для первоначального объекта, но возможно также и создание копий объекта под абсолютно различные ус­ тройства. Например, скажем, вы написали приложение, которое было рассчитано на несколько мониторов, каждый из которых работал в пол­ ноэкранном режиме. Если Mesh-объект отображался на первом монито­ ре, не было необходимости иметь «экземпляр» этого объекта на втором. При желании отобразить объект на втором мониторе, вы можете легко продублировать его (или другими словами создать и отобразить копию объекта) на новое устройство и иметь к нему доступ. Наш предыдущий пример просто использовал уже имеющееся в наличии устройство. Параметр declaration в процедуре Clone определяет, каким образом данные будут отображаться на экране. В используемом нами варианте мы пропускаем описание вершинного формата вновь созданной копии объекта (которая может отличаться от оригинала). В других перегрузках Часть II. Основные концепции построения графики данный параметр предназначен для определения вершин (Vertex declarations) созданного объекта. Как вы можете видеть, это — вспомогательная функция класса Mesh, которая может автоматически вычислять нормали Mesh-объектов. В не­ используемых нами вариантах загрузки присутствует параметр, который используется для ввода информации, имеющий вид массива целых чисел или потока GraphicsStream. Вы можете использовать возможность создания копии объекта в це­ лях создания новых экземпляров того же Mesh-объекта для нового уст­ ройства или изменять опции уже созданного объекта. Вы можете также использовать это для создания объекта с новыми значениями формата вершины, или даже удалять существующие данные формата вершины из вашего Mesh-объекта. Если объект, который есть на данный момент, не удовлетворяет вашим требованиям, вы можете имитировать его, создав копию, и изменить эту копию по необходимости.

Оптимизация данных Mesh-объекта Создание копии Mesh-объекта — не единственный способ расширить свойства имеющегося объекта. Существует еще несколько способов оп­ тимизации Mesh-обьектов. Функция Optimize для объекта похожа на ме­ тод создания копии, описанный выше (данная функция позволяет созда­ вать новый Mesh-объект с различными опциями), однако, она может так­ же выполнять оптимизацию в процессе создания нового объекта. Следу­ ет отметить, что мы не можем использовать функцию Optimize для до­ бавления или удаления вершинных данных или для дублирования их под новое устройство. Рассмотрим основную перегрузку метода Optimize:

public Microsoft.DirectX.Direct3D.Mesh Optimize ( Microsoft.DirectX.Direct3D.MeshFlags flags, int[ ] adjacencyln, out int[ ] adjacencyOut, out int[ ] faceRemap, out Microsoft.DirectX.Direct3D.GraphicsStream vertexRemap ) Каждая из четырех перегрузок этого метода принимает одинаковый набор аргументов, или просто флажков и вводимых данных. Обратите внимание, что параметр adjacencyln может использоваться или как мас­ сив целых чисел (как показано выше), или как поток данных GraphicsStream. Параметр флажков используется, чтобы определить, каким может быть создан новый объект. Данный параметр может иметь разное количество флажков из списка MeshFlags (за исключением флажков Use32Bit или WriteOnly). Флажки Optimize, которые могут использоваться специаль­ ным образом для этой функции, приведены в таблице 7.1:

Глава 7. Использование свойств и возможностей Mesh-объектов Таблица 7.1. Функции оптимизации Mesh-объекта MeshFlags.OptimizeCompact Переупорядочивает поверхности в Mesh-объекте, чтобы удалить неиспользованные вершины и поверхности Переупорядочивает поверхности в Mesh-объекте так, чтобы иметь меньшее количество изменений состояния аттрибута, который может улучшить выполнение DrawSubset MeshFlags.OptimizeAttrSort MeshFlags.OptimizeDevicelndependent Использование этой функции затрагивает размер кэша вершины, определяя заданный по умолчанию размер кэша, и обеспечивает достаточно эффективное выполнение на используемых аппаратных средствах MeshFlags.OptimizeDoNotSplit Использование этого флажка определяет, что вершины не должны быть разбиты в случае, если они распределены между группами атрибутов Оптимизирует только поверхности, игнорируя вершины Использование этого флажка переупорядочивает поверхности, чтобы максимизировать длину смежных треугольников или полигонов Использование этого флажка переупорядочивает поверхности для увеличения скорости кэширования вершины MeshFlags.Optimizelgnore Verts MeshFlags.OptimizeStripeReorder MeshFlags. Optimize VertexCache ОПТИМИЗАЦИЯ MESH-ОБЪЕКТОВ НА МЕСТЕ Что если вы не хотите создавать новый Mesh-объект целиком? Вы не хотите изменять различные флажки в процессе создания, вы только хотите использовать преимущества, полученные при опти­ мизации объекта. Существует похожий метод для Mesh-объекта, называемый OptimizelnPlace, который принимает те же самые параметры, толь­ ко с двумя различиями. Флаговый параметр должен быть одним из Часть II. Основные концепции построения графики флажков оптимизации (вы не можете изменять ни один из флажков создания), и еще, этот метод не имеет возвращаемого значения. Все оптимизации будут проходить непосредственно для объекта, из которого вызывается данная процедура. Также понадобится параметр AdjacencyIn, он может быть представ­ лен в виде целочисленного массива, включающего в себя три целых чис­ ла на каждую сторону, которые определяют трех смежных «соседей» каж­ дой поверхности в объекте, или же в виде потока графических данных, в котором содержится та же самая информация. Если вы выбираете одну из двух последующих перегрузок, последние три параметра в ней представляют те данные, которые будут возвращены вам посредством параметра out. Первым значением этих возвращенных данных будет новая информация смежности. Второе значение — новый индекс для каждой поверхности в объекте. Последним значением будет графический поток, который содержит новый индекс для каждой верши­ ны. Многие из приложений не потребуют этой информации и пропустят эти функции. Следующая небольшая секция кода показывает сортировку объекта с использованием буфера атрибутов и обеспечивает его размещение в уп­ равляемой памяти: // Compact our mesh M s tempMesh = mesh.Optimize eh (MeshFlags.Managed | MeshFlags.OptimizeAttrSort | MeshFlags.OptimizeDoNotSplit, adj) ;

mesh.Dispose();

mesh = tempMesh;

ФОРМИРОВАНИЕ ИНФОРМАЦИИ СМЕЖНОСТИ Обратите внимание, что многие из более совершенных методов Mesh используют информацию смежности. Вы можете получить эту информацию в процессе создания Mesh-объекта. Но если объект уже создан, то для получения этой информации существует функ­ ция GenerateAdjacency. Первый параметр этой функции — число с плавающей точкой, которое можно использовать для того, чтобы определить те вершины, местоположение которых отличается ме­ нее чем на это значение, как смежные или совпадающие вершины. Второй параметр— целочисленный массив, который будет запол­ нен информацией смежности. Этот массив должен иметь размер по крайней мере 3 * mesh.NumberFaces.

Глава 7. Использование свойств и возможностей Mesh-объектов Упрощение существующих Mesh-объектов Теперь предположим, что ваш художник только что нарисовал вам некоторый объект в вашей сцене, который находится в различных поло­ жениях в зависимости от того, на каком уровне вы находитесь. На неко­ торых уровнях объект располагается на заднем плане и не требует столь же детальной деталировки, как на других уровнях. Естественно, можно дать задание художнику, что бы он сделал вам две различные модели, с высоким и низким качеством деталировки, но есть и более простое ре­ шение — можно использовать некоторые встроенные упрощения для имеющегося класса Mesh. Программа упрощения изменяет существующий объект. Суть метода заключается в том, чтобы, используя набор весовых параметров, попробо­ вать удалить как можно больше поверхностей и вершин объекта для полу­ чения низко-детального объекта. Однако, прежде чем упростить объект, его необходимо очистить. Для этого добавляют другую вершину, которая является общей для двух треугольников, расположенных веером (мы уже рассматривали такой тип примитива). Рассмотрим процедуру Clean:

public static Microsoft.DirectX.Direct3D.Mesh Clean ( Microsoft.DirectX.Direct3D.Mesh mesh, Microsoft.DirectX.Direct3D.GraphicsStream adjacency, Microsoft.DirectX.Direct3D.GraphicsStream adjacencyOut out System. String errorsAndWarnings ), Обратите внимание, это напоминает более ранние функции. Процедура аналогичным образом принимает параметр объекта, который мы собира­ емся очистить, и информацию смежности. Однако, помимо этого здесь требуется параметр смежности adjacencyOut. Наиболее общая методика должна использовать смежные данные в графическом потоке, который вы получаете при создании объекта, также как параметры adjacencyIn и adjacencyOut для очистки. Имеется также возвращаемая строка, которая позволяет выводить сообщения об ошибках или предупреждениях, с кото­ рыми можно столкнуться при очистке объекта. Следует также обратить внимание на то, что параметры смежности могут быть или в виде графи­ ческих потоков, как показано выше, или в виде массивов целых чисел. Чтобы показать последствия выполнения программы упрощения, ис­ пользующей эти методы, можно взять пример, который приводился в юнце главы 5, и попробовать использовать его как отправную точку для нашего упрощения. Сначала мы включаем каркасный режим так, чтобы вы могли видеть эффект (на вершинах он более нагляден). Добавим сле­ дующую строку в функцию SetupCamera:

device.RenderState.FillMode = FillMode.WireFrame;

Часть II. Основные концепции построения графики Затем, как мы уже решили, Mesh-объект должен быть очищен. По­ скольку нам необходимо знать информацию смежности объекта для очи­ стки, мы должны переписать код создания объекта в методе LoadMesh следующим образом:

ExtendedMaterial [ ] mtrl;

GraphicsStream adj;

// Load our mesh mesh = Mesh.FromFile(file, MeshFlags.Managed, device, out adj, out mtrl);

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

// Clean our main mesh Mesh tempMesh = Mesh.Clean(mesh, adj, adj);

// Replace our existing mesh with this one mesh.Dispose();

mesh = tempMesh;

Прежде чем мы изменим наш код, чтобы упростить объект, следует вначале рассмотреть метод упрощения Simplify:

public static Microsoft.DirectX.Direct3D.Mesh Simplify ( Microsoft.DirectX.Direct3D.Mesh mesh, int[] adjacency, Microsoft.DirectX.Direct3D.AttributeWeights vertexAttributeWeights float [] vertexWeights, System.Int32 minValue, Microsoft.DirectX.Direct3D.MeshFlags options ), Структура этого метода должна казаться нам знакомой. Объект Mesh, который мы собираемся упрощать, представляет собой первый параметр, параметр смежности adjacency (который может быть определен в виде массива или в виде графического потока) — последующий. Параметр AttributeWeights служит для того, чтобы установить различ­ ные коэффициенты, используемые при упрощении объекта. Для большин­ ства приложений следует использовать процедуры, которые не использу­ ют это значение, поскольку заданная по умолчанию структура рассматри­ вает только геометрические данные и данные нормали. Лишь в отдельных случаях эти значения должны были бы измениться. Значения по умолча­ нию для этой структуры, если вы их не пересылаете, имели бы вид:

AttributeWeights weights.Position weights.Boundary weights.Normal = weights = new AttributeWeights() ;

= l.Of;

= l.Of;

l.Of;

Глава 7. Использование свойств и возможностей Mesh-объектов weights.Diffuse = O.Of;

weights.Specular = O.Of;

weights.Binormal = O.Of;

weights.Tangent = O.Of;

weights.TextureCoordinate = new float[] (O.Of, O.Of, O.Of, O.Of, O.Of, O.Of, O.Of, O.Of };

Следующий параметр vertexWeights — список весовых коэффициен­ тов для каждой вершины. Если вы пересылаете пустой указатель для этого параметра, принимается что каждый вес будет равен l.Of. Далее идет параметр MinValue— минимальное число поверхностей или вершин (в зависимости от пересылаеых флажков), до которого вы хотите попробовать упростить объект. Чем меньше это значение, тем менее детальный объект получится. Однако, следует обратить внимание, что выбор этого значения не подразумевает реальное минимальное значение, которое может быть достигнуто. Другими словами, это значение опреде­ ляет желаемый, а не абсолютный минимум. Последним параметром options для этой функции может быть один из двух флажков. Если вы хотите упростить число вершин, вы должны за­ писать MeshFlags. Simplify Vertex, иначе используйте MeshFlags. SimplifyFace. Теперь необходимо добавить код, чтобы упростить Mesh-объект, ко­ торый мы использовали в нашем примере MeshFile, см. главу 5. Жела­ тельно иметь перед глазами и первоначальный оригинал объекта (очи­ щенный), и упрощенный, а также иметь возможность переключаться от одного к другому. Таким образом, добавим новое поле, чтобы сохранить упрощенный объект:

private Mesh simplifiedMesh = null;

Затем необходимо создать упрощенный объект. В конце функции LoadMesh добавьте следующий код:

II Get our new simplified mesh simplifiedMesh = Mesh.Simplify(mesh, adj, null, 1, MeshFlags.SimplifyVertex);

Console.WriteLine("Number of vertices in original mesh: (0}", mesh.NumberVertices);

Console.WriteLine("Number of vertices in simplified mesh: (0(", simplifiedMesh.NumberVertices);

Pages:     | 1 || 3 | 4 |   ...   | 6 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.