WWW.DISSERS.RU

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

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

Pages:     | 1 | 2 || 4 | 5 |   ...   | 6 |

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

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

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

Часть II. Основные концепции построения графики Number of vertices in original mesh: 4445 Number of vertices in simplified mesh: Таким образом, мы упростили наш объект до примерно 8,8 % от его первоначального размера. Для объектов, находящихся на значительном расстоянии от наблюдателя, достаточно трудно найти различие между высоким и низким разрешением этих объектов, кроме того, данный ме­ тод позволяет более рационально использовать память и увеличить быс­ тродействие. Теперь давайте изменять код таким образом, чтобы мы могли видеть результат упрощения на экране с возможностью переключения от одного объекта к другому. Вначале мы должны добавить булеву переменную, чтобы установить текущий объект, который мы отображаем:

private bool isSimplified = false;

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

device.Material = meshMaterials[i];

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

if (!isSimplified) { mesh.DrawSubset(i) ;

} else { simplifiedMesh.DrawSubset(i);

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

protected override void OnKeyPress(KeyPressEventArgs e) ( if (e.KeyChar == '4' { isSimplified = !isSimplified;

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

private bool isClose = true;

Измените функцию SetupCamera, чтобы иметь возможность отодви­ гать камеру, в зависимости от значения этой булевой переменной: if (isClose) { device.Transform.View = Matrix.LookAtLH(new Vector3(0,0, 580.Of), new Vector3(),new Vector3 (0,1,0));

} else { device.Transform.View = Matrix.LookAtLH(new Vector3(0,0, 8580.Of), new Vector3(), new Vector3(0,1,0));

} //device.RenderState.FillMode = FillMode.WireFrame;

Легко заметить, что камера отодвинута от объекта на 8000 единиц на­ зад. Также обратите внимание, что здесь мы выключили каркасный ре­ жим. Поскольку большинство игр не запускаются в каркасном режиме, ?то даст вам более реалистичное представление. Последняя вещь, кото­ рую мы должны сделать, — обеспечить возможность переключать каме­ ру. Для этого мы будем использовать клавишу «М». Добавьте следующие строки в конце метода OnKeyPress:

if (e.KeyChar == 'm') isClose = !isClose;

Запустите приложение еще раз. Пока камера расположена близко, на­ жмите несколько раз клавишу «пробел». Затем нажмите клавишу «т», чтобы переместить камеру от объекта. Нажмите клавишу «пробел» еще несколько раз и обратите внимание, что теперь эффект гораздо менее дра­ матичен, а результат упрощения при соответствующем положении каме­ ры имеет вполне терпимое качество изображения. На рис. 7.1 и рис.7.2 можно сравнить исходный и упрощенный объекты.

Часть II. Основные концепции построения графики Рис. 7. 1. Близко расположенный объект с исходным высоким разрешением Рис. 7.2. Далеко расположенный объект с низким разрешением Примененная методика может использоваться весьма эффективно, когда необходимо снизить детальное разрешение Mcsh-объекта, и когда нет возможности создать множественные копии одного и того же объек­ та для разных уровней.

Глава 7. Использование свойств и возможностей Mesh-объектов СОХРАНЕНИЕ MESH-ОБЪЕКТОВ Можно заметить, что выполнение этой операции является весьма трудоемким. Сравните время запуска нашего первоначального при­ ложения MeshFile с новым, описанным только что. Если у вашего художника нет возможности создавать отдельные объекты с низким разрешением, вы наверняка не хотите вынудить пользователя тер­ петь каждый раз процесс упрощения. Осталось выбрать золотую середину. Можно выполнять упрощения, используя инструменты, не являющиеся частью основной игры, и сохранять недавно упрощенный объект в свой собственный файл. Это позволит игре по необходимости загружать данные быстро и без повторного выполнения всех процедур упрощения. Если бы мы захотели сохранить нашу упрощенную версию объекта, мы могли бы добавить код, подобный этому:

// Save our simplified mesh for later use // First we need to generate the adjacency for this mesh int[] simpleAdj = new int[simplifiedMesh.NumberFaces * 3];

simplifiedMesh.GenerateAdjacency(O.Of, simpleAdj);

using (Mesh cleanedMesh = Mesh.Clean (simplifiedMesh, simpleAdj, out simpleAdj)) cleanedMesh.Save(@"..\..\Asimple.x", simpleAdj, mtrl, XFileFormat.Text);

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

Часть II. Основные концепции построения графики Объединение вершин в Mesh-объектах Другой эффективный способ упрощения объекта состоит в том, что­ бы объединить «похожие» или общие вершины. Есть метод в классе mesh, называемый WeldVertices, используемый для объединения общих вершин, имеющих одинаковые атрибуты. Прототип метода включает в себя сле­ дующее: public void WeldVertices ( Microsoft.DirectX.Direct3D.WeldEpsilonsFlags flags, Microsoft.DirectX.Direct3D.WeldEpsilons epsilons, Microsoft.DirectX.Direct3D.GraphicsStream adjacencyln, Microsoft.DirectX.Direct3D.GraphicsStream adjacencyOut, out int[] faceRemap, Microsoft.DirectX.Direct3D.GraphicsStream vertexRemap ) Последние четыре параметра уже подробно обсуждались, поэтому мы не будем останавливаться на них еще раз. Они те же самые, какими были в функции Clean. Первые два параметра контролируют то, как различные вершины объединяются вместе. Первый параметр может иметь одно или несколько значений, приведенных в таблице 7.2: Таблица 7.2. Флажки для объединяемых вершин WeldEpsilonsFlags.WeldAll Объединяет все вершины, которые отмечены в данных смежности как перекрываемые Если данная вершина находится в пределах значения epsilon, заданного структурой WeldEpsilons, измените частично перекрываемую вершину так, чтобы она стала идентичной, и, если все компоненты будут равны, то удалите одну из вершин WeldEpsilonsFlags.WeldPartialMatches WeldEpsilonsFlags.DoNotRemove Vertices Может использоваться, только если определена структура WeldPartialMatches. Позволяет только изменение вершины, но не удаление WeldEpsilonsFlags.DoNotSplit Может использоваться, только если определена структура WeldPartialMatches. He позволяет разбивать вершину Глава 7. Использование свойств и возможностей Mesh-объектов Структура самого WeldEpsilons очень похожа на структуру AttributeWeights, используемую для упрощения объекта. Единственное разли­ чие — есть новый параметр для этой структуры, который управляет фак­ тором мозаичности (tessellation). Мозаичность объекта — это параметр, характеризующий изменение уровня детальности объекта путем изменения числа треугольников или полигонов или путем разбиения отдельного треугольника на два и более. В дальнейшем, подразумевая мозаичность, мы будем использовать тер­ мин «тесселяция». Чтобы показать эффект использования этого метода, мы еще раз изме­ ним существующий пример MeshFile с целью объединения его вершин. Поскольку в действительности это только один вызов, нет необходимости в составлении громоздкого кода, который мы должны добавить. Мы будем осуществлять объединение вершин при изначально нажатой клавише «Про­ бел», для этого перепишем метод OnKeyPress следующим образом:

protected override void OnKeyPress(KeyPressEventArgs e) { Console.WriteLine("Before: {0}", mesh.NumberVertices);

mesh.WeldVertices(WeldEpsilonsFlags.WeldAU, new WeldEpsilons (), null, null);

Console.WriteLine("After: {0}", mesh.NumberVertices);

} Теперь нажатие клавиши «Пробел» приведет к следующему текстово­ му сообщению:

Before: 4432 After: В итоге мы получили 91 % вершин от начального количества. Это не 'так впечатляет, как в ранее рассмотренном случае, но зато происходит намного быстрее. Однако, обратили ли вы внимание, как изменились ко­ ординаты текстуры, когда вы нажимали клавишу «пробел»? Это было вызвано удалением некоторых вершин из общей области. Вблизи это не­ много заметнее, но на расстоянии на это можно не обращать внимания. ПРОВЕРКА ДОСТОВЕРНОСТИ ВАШЕГО ОБЪЕКТА Было бы заманчиво иметь способ, позволяющий определить, дей­ ствительно ли ваш объект необходимо очистить, или же он уже го­ тов к оптимизации. Естественно, такой способ есть. Класс Mesh имеет метод Validate, который имеет четыре варианта загрузки. Вот один из них:

Часть II. Основные концепции построения графики System.String errorsAndWarnings ) public void Validate ( Microsoft.DirectX.Direct3D.GraphicsStream adjacency, Есть также перегрузки, которые поддерживают целочисленные мас­ сивы для информации смежности, к тому же вы можете использо­ вать функции с параметром ошибок или без него. Если объект является подходящим, то данный метод будет выпол­ нен успешно, и строка вывода ошибок System.String.Empty (если оп­ ределена) будет пустой. Если же нет (например, индексы недействи­ тельны), то поведение зависит от того, действительно ли строка вывода ошибок определена. Если да, функция возвратит в этой стро­ ке сообщение об ошибке, в противном случае, функция пропустит комментарий. Использование метода Validate перед любым упрощением или оп­ тимизацией помогает устранять отказы, возникающие при выпол­ нении этих методов.

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

// Split information private Mesh[] meshes = null;

private bool drawSplit = false;

private bool drawAHSplit = false;

private int index = 0;

private int lastlndexTick = System.Environment.TickCount;

Эти переменные будут определять созданные в результате разбиения объекты, также как и флажок, указывающий на то, рисуете ли вы исход­ ный объект или отображаете разбиваемый объект (по умолчанию выби­ рается исходный объект). Если вы рисуете разбиваемый объект, имеется второй флажок, указывающий на то, отображаются ли объекты в массиве или по отдельности (по умолчанию следует выбрать второй вариант).

Глава 7. Использование свойств и возможностей Mesh-объектов Необходимо обработать текущий индекс отображаемого объекта (в слу­ чае, если отображение выполняется по отдельности) и «мини-таймер» для определения момента переключения к следующему объекту (для того же случая). Теперь мы можем создавать наш массив объектов. Сразу после вызова LoadMesh в методе InitializeGraphics добавьте следующую секцию кода: meshes = Mesh.Split(mesh, null, 1000, mesh.Options.Value);

Мы должны рассмотреть различные параметры функции разбиения. Имеются два варианта загрузки, мы выбираем наиболее полный: public static Microsoft.DirectX.Direct3D.Mesh[ ] Split (Mesh mesh, int[ ] adjacencyln, System.Int32 maxSize, MeshFlags options, out GraphicsStream adjacencyArrayOut, out GraphicsStream faceRemapArrayOut, o t G a h c S r a vertRemapArrayOut ) u rpistem Видно, что эта функция принимает в качестве первого параметра объект, который мы хотим разбить. Следующий параметр — adjacencyln, данные о смежности (можно переслать пустой указатель null, поскольку мы уже описывали его). Третий параметр maxSize — максимальное чис­ ло вершин в создаваемых объектах. Для предыдущего примера, макси­ мум составляет 1000 вершин на каждый новый объект. Параметр options используется, чтобы определить флажки для каждого из недавно создан­ ных объектов. Последние три параметра возвращают информацию о каж­ дом из новых объектов, в виде потока данных. Мы будем использовать перегрузки без этой информации. После того как мы создали массив объектов, необходимо задать спо­ соб переключения от исходного объекта к полученному массиву. Для этого будем использовать те же самые клавиши, что и ранее: клавиша «Про­ бел» и «М». Замените метод OnKeyPress следующим кодом: protected override void OnKeyPress(KeyPressEventArgs e) { if (e.KeyChar == ' ') { drawSplit = !drawSplit;

} else if (e.KeyChar == V) { drawAHSplit = IdrawAllSplit;

} } Часть II. Основные концепции построения графики Здесь мы переключаем два режима отображения — отображение ис­ ходного и разбиваемого объекта, после чего выбираем отображение раз­ биваемого объекта целиком или же по отдельности. Теперь мы должны изменить метод DrawMesh, чтобы использовать эти переменные. Заме­ ните содержание указанного метода в соответствии с кодом, приведен­ ным в листинге 7.2.

Листинг 7.2. Рендеринг разбиваемых объектов. if ((System.Environment.TickCount - lastlndexTick) > 500) { index++;

if (index >= meshes.Length) index = 0;

lastlndexTick = System.Environment.TickCount;

} 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.SetlexturefO, meshTextures[i]) ;

if (drawSplit) { if (drawAllSplit) { foreach(Mesh m in meshes) m.DrawSubset(i);

} else { meshes[index].DrawSubset(i);

} } else { mesh.DrawSubset (i);

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

Глава 7. Использование свойств и возможностей Mesh-объектов При запуске данного приложения вначале отобразится исходный объект. После нажатия на клавишу «Пробел» начнут отображаться от­ дельные разбитые объекты. Если вы в этом режиме нажмете клавишу «М», отобразятся все разбитые объекты одновременно. Вы можете срав­ нить исходный и полученный результат.

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

Часть II. Основные концепции построения графики Глава 8. Введение в ресурсы Ресурсы (Resources) являются основополагающей частью отображе­ ния или рендеринга сложных объектов в Direct3D. В этой главе мы рас­ смотрим более совершенные возможности ресурсов, включая. • Статические и динамические ресурсы. Изменение содержимого буферов, включенных в Mesh-объекты. Использование различных флажков блокировки. Использование небезопасного режима «unsafe» для максималь­ ного выполнения операций.

Класс ресурсов Класс Resource — относительно маленький класс, но он является базо­ вым для всех ресурсов в Direct3D, так что имеет смысл начать рассмотре­ ние этого раздела книги со списка и назначения каждого метода этого класса. В действительности мы никогда не будем создавать или использовать этот класс непосредственно, так как он является абстрактным, но мы сможем использовать различные методы этого класса, приведенные в таблице 8.1. Таблица 8.1. Методы Класса «Resource» и параметры Параметр или метод Описание Device Туре Только для чтения;

возвращает устройство для этого ресурса Только для чтения, возвращает тип ресурса. Подходящие значения для этотого параметра находятся в списке ResourceType Чтение — запись, возвращает приоритет этого ресурса. Предполагаются только те ресурсы, которые постоянно находятся в управляемом пуле памяти. Они используются, чтобы определить, когда ресурс может быть удален из памяти. Более низкий приоритет означает более быстрое удаление ресурса из памяти. По умолчанию все управляемые ресурсы имеют приоритет О, а все неуправляемые ресурсы будут всегда иметь приоритет 0. Есть также метод SetPriority, который возвращает старый приоритет после назначения нового Используйте этот метод для индикации того, что управляемый ресурс может вскоре понадобиться. Это позволит приложению Direct3D перемещать ваш ресурс в видеопамять еще до того, как это будет необходимо. Важно обратить внимание, что если вы уже используете значительное количество видеопамяти, этот метод будет неэффективен Priority PreLoad Глава 8. Введение в ресурсы Параметр или метод Описание PrivateData members Имеются три значения, которые позволяют получать и записывать индивидуальные данные для каждого из ресурсов. Эти данные никогда не используются Direct3D и могут использоваться приложением по вашему усмотрению Как вы можете видеть, класс Resource имеет дело главным образом с расширенными возможностями управляемых ресурсов. Важно обратить внимание на то, что каждый ресурс в Direct3D, также как и пул памяти (определенный в списках Usage и Pool), будет иметь директиву using. Рассмотренные параметры и свойства определяют, каким образом исполь­ зуется ресурс, и где он расположен в памяти (будь это системная память, видеопамять или AGP память). Ресурсы позволяют центральному процессору обращаться к сохранен­ ным данным обычно с помощью механизма блокировки Locking. В на­ ших примерах до настоящего времени мы формировали наши буферы, каждый раз вызывая метод SetData, и создавали текстуры из файлов. Однако, можно извлекать данные или небольшие подразделы этих дан­ ных, блокируя на время эти ресурсы. Мы кратко обсудим эти вопросы в этой главе. Давайте пока рассмотрим первый из ресурсов, которые мы представили.

Использование вершинных и индексных буферов Вершинные буферы являются основной структурой данных, исполь­ зуемых приложением Direct3D для сохранения данных о вершинах, в то время как индексные буферы играют туже роль, но для индексов. Эти • классы происходят от класса Resource и, таким образом, наследуют ме­ тоды этого класса, добавляя несколько своих собственных. Одним из главных параметров класса буферов является описание бу­ фера (description). Структура, возвращаемая из этого раздела, сообщает всю информацию о созданном буфере, включая формат, использование, пул памяти, размер и формат вершины. Возможно, мы бы уже знали эту информацию, если бы создавали буферы самостоятельно, но при созда­ нии буфера из внешнего источника это весьма полезная информация. СТАТИЧЕСКИЕ И ДИНАМИЧЕСКИЕ БУФЕРЫ Описав два этих метода, связанных с классом буферов, мы можем обсудить различия между статическими и динамическими буфера­ ми. Это касается и вершинных, и индексных буферов.

Часть II. Основные концепции построения графики Если буфер был создан с флажком Usage.Dynamic— это динами­ ческий буфер, любой другой буфер является статическим. Стати­ ческие буферы предназначены для элементов, значения которых изменяются не так часто, в то время как динамические буфера опе­ рируют с постоянно изменяющимися данными. Статический буфер, тем не менее, не подразумевает, что его дан­ ные не могут изменяться. Однако, блокировка и обмен информаци­ ей со статическим буфером во время его использования может при­ вести к проблемам в процессе выполнения приложения. Прежде чем данные можно будет изменить, процессор должен закончить обра­ ботку текущего набора данных, что может потребовать определен­ ного времени. С другой стороны, когда данные в буфере изменяют­ ся, процессор ждет окончания этих действий, что также влияет на эффективность не с лучшей стороны. Если вы предполагаете частое изменение данных, необходимо ис­ пользовать динамический буфер (флажок Usage.Dynamic), это по­ зволит приложению Direct3D оптимизировать конвейер данных для их частого изменения. Но следует отметить, что использование ди­ намического буфера не даст никаких преимуществ в скорости по сравнению со статическим вариантом. Нам желательно знать количество ожидаемых элементов в буферах на кадр и соответственно под это выбирать буфер. Для максималь­ ного быстродействия рекомендуется иметь один большой статичес­ кий вершинный буфер для каждого отображаемого формата вер­ шин, а при необходимости возможно использование динамических буферов.

Механизм Locking, блокировка используемых буферов Механизм блокировки locking кажется одним из наиболее неясных моментов в Direct3D, особенно в Управляемом DirectX. Как выполнять эти процедуры и как увеличивать их быстродействие — достаточно се­ рьезный вопрос для тех, кто хотел бы это использовать. Прежде чем мы возьмемся за это, выясним что же такое «блокировка» буфера? В действительности это просто действие, разрешающее доступ центрального процессора к разделу данных в вашем ресурсе, при этом ресурс блокируется от каких-либо внешних «вмешательств». Поскольку вы не можете непосредственно «общаться» с графическими устройства­ ми, необходимо найти способ управлять данными вершин из вашего при­ ложения, для этого и нужен механизм блокировки. Рассмотрим различ­ ные процедуры или методы блокировки, которые существуют для вер­ шинных и индексных буферов (они принимают те же самые параметры):

Глава 8. Введение в ресурсы public System.Array Lock ( System.Int32 offsetToLock, Microsoft.DirectX.Direct3D.LockFlags flags ) public System.Array Lock ( System.Int32 offsetToLock, System.Type typeVertex, Microsoft.DirectX.Direct3D.LockFlags flags, params int[] ranJcs ) public Microsoft.DirectX.Direct3D.GraphicsStream Lock ( System.Int32 offsetToLock, System.Int32 sizeToLock, Microsoft.DirectX.Direct3D.LockFlags flags ) Как вы можете видеть, имеется три вида перегрузки, предназначен­ ных для блокировки наши буферов. Начнем с первой, как самой простой. Эта перегрузка доступна, только если вы создавали буфер с помощью конструктора, который принимает описание System.Type и значения вер­ шин или индексов. Остальные две перегрузки представляются весьма интересными. Первый параметр каждой из них — offset, смещение (в байтах), с кото­ рого вы хотите запустить механизм блокировки. Если вы желаете бло­ кировать весь буфер, необходимо установить нулевое значение смеще­ ния. Обратите внимание, что первые два варианта процедуры возвра­ щают массив данных. Во втором варианте вторым параметром являет­ ся исходный тип возвращаемого массива. Последний параметр ranks используется для определения размера или ранга возвращаемого мас­ сива. Для примера, давайте предположим, что у вас есть вершинный буфер (Vector3), включающий в себя координатные данные имеющихся 1200 вершин. Блокировка данного буфера выглядела бы следующим образом:

Vector3[] data = (Vector3[])vb.Lock(0, typeof(Vector3), LockFlags.None, 1200);

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

Vector3[,] data = (Vector3[,])vb.Lock(0, typeof(Vector3), LockFlags.None, 600, 600);

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

Часть II. Основные концепции построения графики БОЛЕЕ ЭФФЕКТИВНАЯ БЛОКИРОВКА БУФЕРОВ Параметр LockFlags будет задействован при последнем варианте вызова процедуры, но сначала следует сказать несколько слов о не­ достатках перегрузок с возвращаемыми массивами. Речь идет об эффективности и быстродействии. Предположим, у вас имеется созданный буфер с заданными по умолчанию опциями, и никаких флажков блокировки. При вызове метода блокировки будет проис­ ходить следующее. • Данные вершины блокируются;

данные в ячейке памяти сохра­ нены. • Новый массив подходящего типа распределен, дан размер, ука­ занный в параметре ranks. • Данные скопированы из блокированной ячейки памяти в наш но-' вый буфер. • Данный буфер возвращен пользователю, и при необходимости может быть изменен. • При последующем вызове метода Unlock, уже другая копия данных из массива пересылается назад в блокированную ячей­ ку памяти. • И окончательно все данные вершины разблокируются. Не трудно понять, почему этот метод выполняется несколько мед­ леннее, чем мы этого ожидали. Можно удалять только одну из двух копий: определяя при создании буфера флажок «только для запи­ си» Usage.WriteOnly, мы устраняем первую копию, а, определяя при блокировке флажок «только для чтения» LockFlags.Readonly, — вторую. Последний вариант вызова является наиболее предпочтительным. Он также имеет новый параметр, а именно, размер данных, кото­ рые мы хотим блокировать. В других перегрузках это значение вы­ числяется в течение вызова по формуле sizeof(type)*NumberRanks. Если вы используете последнюю перегрузку, просто пересылайте размер данных, которые вы хотите блокировать (в байтах). Если вы хотите блокировать весь буфер, установите в первых двух парамет­ рах этого метода нулевые указатели. Эта перегрузка возвратит класс GraphicsStream, который позволит вам непосредственно управлять заблокированными данными без любых дополнительных распределений памяти для массивов и без любых дополнительных копирований в память. Теперь вы можете непосредственно и более эффективно управлять памятью, выигры­ вая в быстродействии. Но, тем не менее, не стоит полностью отказываться от операций бло­ кировки с использованием массивов из-за того, что они более мед­ ленные. Они достаточно удобны и, если их использовать надлежа Глава 8. Введение в ресурсы щим образом, проигрыш в быстродействии по сравнению с опера­ циями блокировки потоковых данных GraphicsStream практически незаметен.

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

LockFlags.None LockFlags.Discard LockFlags.NoOverwrite LockFlags.NoDirtyUpdate LockFlags.NoSystemLock LockFlags.Readonly Если вы не используете флажки блокировки, будет задействован ме­ ханизм блокировки по умолчанию. Если вы хотите управлять процеду­ рой блокировки, можно использовать флажки, приведенные выше. Флажок Discard (исключение) может использоваться только для дина­ мических буферов. В этом случае вершинный или индексный буфер бу­ дет, и, если существует еще какое-либо подключение к старой памяти, будет возвращен новый блок памяти. Этот флажок весьма полезен, если вы заполняете вершинный буфер динамически. Как только вершинный буфер полностью заполняется, вы блокируете его, используя данный фла­ жок. Эта процедура часто используется в комбинации со следующим ти­ пом флажков. Флажок NoOverwrite (также используется только для динамических буферов) сообщает приложению Direct3D, что вы не будете перезапи­ сывать данные в вершинном или индексном буфере. Использование дан­ ного флажка позволит отменить или переопределить вызов и продол­ жить использовать буфер. Если вы не используете этот флажок, блоки­ ровка будет выполняться до тех пор, пока не завершится любой теку­ щий рендеринг. Поскольку при этом вы не можете перезаписывать дан­ ные в этот буфер, это будет полезно только при добавлении вершин в буфер. Стандартное использование этих двух флажков подразумевает исполь­ зование буфера большого размера, который заполняется динамически. Флажок NoOverwrite используется до тех пор, пока буфер не заполнится полностью. В это время следующая блокировка будет использовать фла­ жок Discard для блокировки буфера. В листинге 8.1. приводится пример кода из процедуры PointSprites (точечные спрайты), поставляемой вмес­ те с DirectX SDK.

Часть II. Основные концепции построения графики Листинг 8.1. Выборка из метода PointSprites. if (++numParticlesToRender == flush) { // Done filling this chunk of the vertex buffer. Lets unlock and // draw this portion so we can begin filling the next chunk. vertexBuffer.Unlock();

dev.DrawPrimitives(PrimitiveType.PointList, baseParticle, numParticlesToRender);

// Lock the next chunk of the vertex buffer. If we are at the // end of the vertex buffer, LockFlags.Discard the vertex buffer and start // at the beginning. Otherwise, specify LockFlags.NoOverWrite, so we can // continue filling the VB while the previous chunk is drawing. baseParticle += flush;

if (baseParticle >= discard) baseParticle = 0;

vertices = (PointVertex[])vertexBuffer.Lock(baseParticle * DXHelp.GetTypeSize(typeof(PointVertex)), typeof(PointVertex), (baseParticle != 0) ? LockFlags.NoOverwrite : LockFlags.Discard, flush);

count = 0;

numParticlesToRender = 0;

} В этой выборке регистрируется момент, когда данные полностью на­ копились и готовы к отображению на экране (вызов DrawPrimitives). Если такой момент наступил, мы разблокируем наш буфер и производим рен­ деринг. Затем мы определяем, действительно ли мы находимся в конце нашего буфера (baseParticle > = discard), и блокируем наш буфер еще раз. Мы запускаем блокировку в конце буфера, пересылая флажок NoOverwrite, за исключением случая, когда baseParticle имеет ненулевое значение, тогда мы блокируем данные в начале буфера, используя фла­ жок Discard. Это позволяет нам продолжить добавление новых точечных спрайтов к нашей сцене, пока буфер не заполнится, после чего мы ис­ ключим наш старый буфер и начнем заполнять новый. Рассмотрев эти два типа флажков, мы можем передти к оставшимся. Самый простой из них — Readonly, который, судя по его названию, со­ общает Direct3D, что вы не будете записывать данные в буфер. Когда мы имеем дело с перегрузками блокировки, которые возвращают массивы, данная операция имеет некоторые преимущества, поскольку не происхо­ дит копирования данных, обновляемых при вызове разблокировки Unlock. Флажок NoDirtyUpdate предотвращает любое изменение ресурса, на­ ходящегося в недействительной области. Без этого флажка блокировка ресурса автоматически приведет к добавлению недействительной облас­ ти к этому ресурсу.

Глава 8. Введение в ресурсы Последний из флажков — NoSystemLock, как правило, используется нечасто. Обычно, когда вы блокируете ресурс в видеопамяти, резервиру­ ется критическая секция «systemwide» в рамках всей системы, не позволяя каким-либо образом изменять режимы визуального отображения до тех пор, пока действует блокировка. Использование флажка NoSystemLock отключает это свойство. Данное действие может быть полезно только для операций блокировки, занимающих весьма непродолжительное время, и только если вы все еще нуждаетесь в увеличении скорости выполнения. Таким образом, часто использовать данный флажок не рекомендуется. Естественно, после вызова блокировки и после выполнения необхо­ димых операций мы должны разблокировать буфер. Для этого использу­ ется механизм разблокировки. У этой функции нет никаких параметров, разблокировка должна вызываться каждый раз, когда используется бло­ кировка и наоборот, невыполнение этого требования может вызвать ошиб­ ки при процедуре рендеринга.

Использование текстурных ресурсов Все ресурсы текстур в Управляемом DirectX происходят от отдельно­ го класса BaseTexture, принадлежащего общему классу Resource. Суще­ ствует несколько новых методов, основанных на использовании объек­ тов класса BaseTexture, см. таблицу 8.2. Таблица 8.2. Методы класса BaseTexture и параметры Параметр или метод AutoGenerateFilterType Описание Параметр чтения-записи, описывает, каким образом mipmap подуровни сгенерированы автоматически. Он принимает значение списка TextureFilter (фильтра текстур), и автоматически воссоздает любые mipmap подуровни, если тип фильтра изменен. Заданный по умолчанию тип фильтра для генерации mipmap уровня — TextureFilter.Linear (линейная фильтрация), в случае если драйвер не поддерживает этот режим, будет использоваться TextureFilter.Point (точечная фильтрация). Если устройство не поддерживает выбранный тип фильтра, процедура фильтрации игнорируется. Все параметры фильтра являются используемыми, за исключением значения «None». Можно видеть, что значение TextureFilterCaps структуры Capabilities (Возможности) определяет, какие типы фильтров поддерживаются вашим устройством Параметр чтения-записи, определяет наиболее детальный (в плане разрешения) уровень управляемых текстур LevelOfDetail 170 Параметр или метод GenerateMipSubLevels Часть II. Основные концепции построения графики Описание Этот метод автоматически генерирует подуровни текстуры mipmap, о которой мы только что упомянули. Он будет использовать следующее свойство, чтобы определить режим фильтрации и использовать его при создании этих уровней Только для чтения, определяет число уровней используемой текстуры. Например, текстура mipmapped (текстура с разрешением, изменяющимся по мере удаления отображаемого объекта, см. ниже в «Структура Mipmaps»), имела бы число уровней, определяемое значением LevelCount LevelCount СТРУКТУРА MIPMAPS Проще говоря, mipmap представляет собой цепочку или последо­ вательность текстур, первая из текстур имеет самое большое зна­ чение детального разрешения. Значение разрешения каждой пос­ ледующей текстуры уменьшается в два раза от значения предыду­ щей. Например, при значении разрешения первой текстуры 256x256, следующий уровень будет иметь 128x128, затем 64x64 и так далее, по мере удаления объекта от наблюдателя. Цепочки Mipmap используются приложением Direct3D для управления каче­ ством отображаемых текстур, что требует значительного места в памяти. Данный подход позволяет существенно экономить ресур­ сы памяти при сохранении приемлемого разрешения в целом. При создании текстуры один из используемых параметров опреде­ ляет число уровней, которые вы хотите иметь в этой текстуре. Это число уровней связано непосредственно с цепочкой текстур, кото­ рые были описаны выше. При задании нулевого значения для этого параметра приложение Direct3D автоматически создаст набор mipmaps от вашей первоначальной текстуры с максимальной разре­ шающей способностью до последней, имеющей разрешение 1x1. В нашем примере, используя «0» для этого параметра, и имея перво­ начальное разрешение 256x256, будет создана цепочка из девяти текстур: 256x256, 128x128, 64x64, 32x32, 16x16, 8x8, 4x4, 2x2, и 1x1. При вызове функции SetTexture приложение Direct3D будет автома­ тически проводить фильтрацию различных текстурируемых цепочек mipmaps, основываясь на текущей установке свойств MipFilter в классе Sampler. Если вспомнить, в нашей игре «Dodger», которую мы написали в главе 6, мы устанавливали фильтры растяжения и сжатия для наших текстур дороги. Для фильтров mipmap использу­ ются те же подходы.

Глава 8. Введение в ресурсы Блокировка текстур и получение описаний Подобно вершинным и индексным буферам, текстуры также имеют механизм блокировки. Дополнительно к известным нам методам здесь появляются две новые особенности, которые не присущи ресурсам гео­ метрии (вершины, нормали и пр.), а именно: недействительные области (dirty regions) и фоновые объекты (backing object). Для начала изучим механизм блокировки для текстур. Блокировка текстур сходна с подобной операцией над нашими «гео­ метрическими» буферами с двумя имеющимися различиями. Во-пер­ вых, вызов блокировки принимает параметр уровня level множествен­ ной текстуры, по отношению к которому мы хотим выполнить блоки­ ровку. Во-вторых, вы можете заблокировать отдельно полигон (к при­ меру, куб для объемных текстур), или же блокировать всю поверхность текстуры, используя при этом перегрузку без определения этого пара­ метра. При работе с кубическими текстурами, необходим еще один дополни­ тельный параметр — сторона отображаемого куба, которую вы хотите заблокировать. В качестве этого параметра вы можете использовать зна­ чение из списка CubeMapFace. Обратите внимание, что некоторые из процедур блокировки полиго­ нов LockRectangle могут возвращать параметр шага (pitch). Это дает воз­ можность узнать о количестве данных в каждом ряду (в байтах). В то время как блокировки текстуры могут принимать параметр «type» для блокировки определенных типов данных, сами текстуры непосред­ ственно сохраняют только информацию о цветах. Если вы желаете ис­ пользовать одну из перегрузок, использующих тип, вы должны убедить­ ся, что данный тип имеет тот же самый формат, что и формат пиксела текстуры. Теперь мы рассмотрим блокировку текстуры на примере. Будем ис­ пользовать знакомый нам MeshFile. Мы удалим текстуру, которая исполь­ зуется для рендеринга, и заменим ее на другую, созданную динамически для нашего приложения. Нам потребуется два режима для этой тексту­ ры: тот, который циклически повторяет текстуры, и тот, который являет­ ся абсолютно случайным. Для этого нам понадобятся следующие пере­ менные:

private uint texColor = 0;

private bool randomTextureColor = false;

Мы планируем использовать 32-разрядный формат пиксела для на­ шей текстуры, таким образом, мы сохраняем наш цвет также в 32-раз­ рядном формате («uint», целочисленный). С помощью булевой перемен­ ной мы выбираем, либо мы создаем случайные цвета текстуры, либо ис Часть II. Основные концепции построения графики пользуем циклически повторяющуюся текстуру. Теперь мы должны со­ здать метод, который будет выполнять блокировку текстуры и ее обнов­ ление или изменение. Чтобы показать различия между методами с ис­ пользованием массивов (array methods) и небезопасными методами (unsafe methods), рассмотрим оба случая. Метод с использованием массива при­ веден в листинге 8.2.

Листинг 8.2. Заполнение текстуры с использованием массива. private void FillTexture(Texture t, bool random) { SurfaceDescription s = t.GetLevelDescription(O);

Random r = new Random() ;

uint[,j data = (uint[,])t.LockRectangle(typeof(uint), 0, LockFlags.None, s.Width, s.Height);

for (int i = 0;

i < s.Width;

i++) ( for (int j = 0;

j < s.Height;

j++) ( if (random) ( } else { data[i,j] = (uint)Color.FromArgb( r.Next(byte.MaxValue), r.Next(byte.MaxValue), r.Next (byte.MaxValue)).ToArgb() ;

data[i,j] = texColor++;

if (texColor >= OxOOffffff) texColor = 0;

t. OnlockRectangle(0);

} Здесь мы берем существующую текстуру и сначала получаем ее опи­ сание, чтобы узнать и задать размер данных для заполнения. Зная шири­ ну и высоту данных, мы блокируем данные в двухмерном массиве (ши­ рина/высота). При блокировке данного массива используется целое чис­ ло без знака (поскольку используется 32-разрядный формат пиксела), при этом информация о флажках блокировки не важна. Так как мы создаем только один уровень mipmap последовательности, мы определяем значе­ ние для блокировки нулевого уровня в текстуре.

Глава 8. Введение в ресурсы После того, как текстура блокирована, перебираются значения воз­ вращенного массива текстуры, ее цвета изменяются соответственно либо случайным образом, либо с помощью приращения (цвет нашей цикли­ ческой текстуры). И, наконец, после того как эти данные полностью из­ менены, мы разблокируем нашу текстуру. Рассмотрим, что же происходит при использовании небезопасного метода, вариант которого приведен в листинге 8.3.

Листинг 8.3. Заполнение текстуры с использованием небезопасного метода. private unsafe void FillTextureUnsafe(Texture t, bool random) { SurfaceDescription s = t.GetLevelDescription(O);

Random r = new Random() ;

uint* pData = (uint*)t.LockRectangle(0, LockFlags.None).IntemalData.ToPointer() ;

for (int i = 0;

i < s.Width;

i++) { for (int j = 0;

j < s.Height;

j++) { if (random) { *pData = (uint)Color.FromArgb( r.Next(byte.MaxValue), r.Next(byte.MaxValue), r.Next(byte.MaxValue)).ToArgb ();

} else { *pData = texColor++;

if (texColor >= OxOOffffff) texColor = 0;

} pData++;

} } t.UnlockRectangle(O);

} Обратите внимание, что единственные отличия этой функции — за­ дание небезопасного режима и то, как мы изменяем наши данные. Мы блокируем буфер данных потока и используем значение IntemalData (как указатель IntPtr), чтобы получить доступ к данным, которые мы будем изменять. Мы управляем данными непосредственно через указатель Pointer, и в конце операции вновь разблокируем нашу текстуру.

Часть II. Основные концепции построения графики Чтобы компилировать этот метод, вы должны включить в свойствах проекта возможность компиляции небезопасного кода. Для этого в раз­ деле Configuration Properties, установите значение «true» для свойства Allow Unsafe Code Blocks. Теперь, когда мы имеем функцию, позволяющую заполнить текстуру различными цветами, необходимо изменить функцию LoadMesh, чтобы использовать данную функцию, а не пытаться создавать текстуры через файл. Замените код создания материала в методе LoadMesh следующим кодом:

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

i < mtrl.Length;

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

meshTextures[i] = new Texture(device, 256, 256, 1, 0, Format.X8R8G8B8, Pool.Managed);

#if (UNSAFE) FillTextureUnsafe(meshTextures [i], randomTextureColor);

#else FillTexture(meshTextures [i], randomTextureColor);

#endif } Как вы можете видеть, мы больше не пытаемся загружать текстуру из файла. Для каждой текстуры в нашем массиве мы создаем новую текстуру, используя формат пиксела X8R8G8B8 (32-разрядный фор­ мат). Мы создаем текстуру 256x256 с одним только уровнем. Затем мы заполняем нашу текстуру, используя только что описанный метод и оператор #if для выбора. Вы можете так же определить выбор Unsafe в вашем файле кода и использовать его вместо предложенного вари­ анта. ВРАЩЕНИЕ С ПРИВЯЗКОЙ К ЧАСТОТЕ СМЕНЫ КАДРОВ Следует упомянуть один момент, на котором мы уже останавлива­ лись раньше. Необходимо вспомнить то, как случайно изменялись цвета текстуры и скорость вращения модели при нажатии на клави­ шу. Это связано с тем, что движение модели в этом случае зависит от скорости смены кадров в большей степени, чем от системного таймера. Имея вновь созданную с помощью новой функции текстуру, вы може­ те выполнять приложение и создавать более красочные модели. Для об­ новления текстуры необходимо вызывать метод заполнения текстуры Глава 8. Введение в ресурсы каждый раз при смене кадра. Кроме того, мы можем устанавливать про­ смотр нашей текстуры. Перепишите метод SetupCamera, см. листинг 8.4, вставив туда нашу функцию.

Листинг 8.4. Установка опций камеры. private void SetupCamera() { 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)) ;

device.RenderState.Lighting = false;

if (device.DeviceCaps.TextureFilterCaps.SupportsMinifyAnisotropic) device.SamplerState[0].MinFilter = TextureFilter.Anisotropic;

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

if (device.DeviceCaps.TextureFilterCaps.SupportsMagnifyAnisotropic) device.SamplerState[0].MagFilter = TextureFilter.Anisotropic;

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

foreach(Texture t in meshTextures) #if (UNSAFE) FillTextureUnsafe (t, randomTextureColor) ;

#else FillTexture(t, randomTextureColor);

#endif } Мы выключили освещение для того, чтобы видеть разнообразие на­ ших цветов без какого-либо эффекта затенения, и включили текстурный фильтр (если он поддерживается). Последнее, что необходимо сделать, — каким-то образом включить случайные цвета текстуры. Будем использо­ вать нажатие любой клавиши в качестве переключателя между двумя режимами текстурирования. Для этого добавьте следующее:

protected override void OnKeyPress(KeyPressEventArgs e) { randomTextureColor = ! randomTextureColor;

} Использование случайного подбора цветов создает эффект анимации. Раскраска объекта на экране напоминает белый шум или картинку в те­ левизоре при отсутствии принимаемого сигнала, см. рис.8.1.

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

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

private SimplificationMesh simplifiedMesh = null;

private float cameraPos = 580.Of;

Переменная, описывающая глубину размещения камеры, позволит лег­ ко изменять ее местоположение. Мы также должны преобразовать коорди­ наты для правильного размещения камеры в сцене. В методе SetupCamera перепишите процедуру преобразования следующим образом:

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

Очевидно, что, поскольку метод SetupCamera вызывается каждый раз при смене кадра, необходимо также каждый раз переписывать перемен Часть II. Основные концепции построения графики ную местоположения камеры. Эта переменная находится там же, где мы записываем данные при упрощении объекта. Следует обратить внима­ ние на то, что мы не имеем никаких дополнительных mesh-объектов. Каждый раз, упрощая наш объект, мы будем просто заменять его новым. Необходимо переписать метод LoadMesh, чтобы удостовериться, что наш объект очищен, и упрощенный объект создан должным образом. Мы должны заменить этот метод на приведенный в листинге 9.1:

Листинг 9.1. Создание упрощенного объекта Mesh. private void LoadMesh(string file) ( ExtendedMaterial[] mtrl;

GraphicsStream adj ;

// Load our mesh mesh = Mesh.FromFile(file, MeshFlags.Managed, device, out adj, 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].Materia3D;

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);

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

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

mesh = tempMesh;

// Create our simplification mesh simplifiedMesh = new SimplificationMesh(mesh, adj);

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

Глава 9. Применение других типов Mesh Практически, это все, что было необходимо выполнить. Осталось толь­ ко добавить код, позволяющий нам перемещать объект дальше от каме­ ры, и уменьшать детальное разрешение объекта. Расстоянием до камеры мы будем управлять с помощью клавиатуры, см. листинг 9.2:

Листинг 9.2. Обработчик событий для операции KeyPress. protected override void OnKeyPress(KeyPressEventArgs e) ( if (e.KeyChar == '+') { cameraPos += (MoveAmount * 2 ) ;

simplifiedMesh.ReduceFaces(mesh.NumberFaces - MoveAmount);

simplifiedMesh.ReduceVertices(mesh.NumberVertices - MoveAmount);

mesh.Disposed;

mesh = simplifiedMesh.Clone(simplifiedMesh.Options.Value, simplifiedMesh.VertexFormat, device);

} if (e.KeyChar == 'W') device.RenderState.FillMode = FillMode.WireFrame;

if (e.KeyChar == 'S') device.RenderState.FillMode = FillMode.Solid;

} Обратите внимание, что здесь мы используем неопределенную констан­ ту, чтобы управлять необходимым перемещением, активизируемым с помо­ щью нажатия клавиши. Можно определить константу следующим образом: private const int M v A o n = 100;

oe m u t В описываемом методе нажатие клавиши «W» переключает выполнение приложения в каркасный режим WireFrame, который достаточно нагляден для того, чтобы «разглядеть» полигоны на отображаемом объекте. Нажатие клавиши «S» переключит нас обратно к сплошному отображению объекта. При нажатии клавиши «+» мы будем отодвигать камеру от нашей модели до определенного значения. Удаляя объект от камеры, мы уменьшаем число сторон и вершин в соответствии с указанной константой MoveAmount. Для того чтобы отобразить наш упрощенный объект, необходимо использовать его в методе рендеринга вместо первоначального объекта. Выполняя приложение и нажимая клавиши «+» и «W» (соответствен­ но приближая или удаляя объект и переключая отображение от каркас­ ного исполнения к сплошному), мы практически не увидим разницы меж­ ду отображаемыми объектами. Теперь можно добавить сопровождающий текст к нашей сцене, чтобы показать число сторон и вершин отображае­ мого в данный момент объекта. Для управления шрифтом добавьте сле­ дующую переменную:

Часть II Основные концепции построения графики private Microsoft.DirectX.Direct3D.Font font = null;

Мы также должны инициализировать наш шрифт прежде, чем начать отображать eго. После вызова LoadMesh в методе IntializeGraphics до­ бавьте код инициализации: // Create our font font = new Microsoft.DirectX.Direct3D.Font(device, new System.Drawing.Font "Arial", 14.Of, FontStyle.Bold | FontStyle.Italic));

Изменяя и и параметры можно выбирать различный тип, размер н стиль шрифта. И, наконец, осталось отобразить лот текст. После вызова DrawMesh можно добавить следующий код: • font.DrawText(null, string.Format("Number vertices in mesh: mesh.numberVertices), new Rectangle (10, 10, 0, 0), DrawTextFormat.NoClip, Color.BlanchedAlmond);

font.DrawText(null, string.FormatСНивЬег faces in mesh: (0)*, mesh.HumberFaces), new Rectangle(10, 30, 0, 0), DrawTextFormat.UoClip, Color.BlanchedAlmond);

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

Рис. 9.1 Упрощенный Меsh-объект Глава 9. Применение других типов Mesh Одна из основных проблем, с которой придется столкнуться в данном приложении, заключается в том, что при обратном или повторном при­ ближении камеры к объекту мы не можем восстановить потерянные в процессе упрощения вершины. Для данного случая нет никакого метода восстановления после использования, к примеру, метода упрощения ReduceVertices. Объекты и функции упрощения SimplificationMesh разработаны и пред­ назначены только для того, чтобы упростить объект, без обратного его восстановления. Для решения этой проблемы существуют более совершенные объек­ ты Mesh.

Управление степенью детализации, класс прогрессивных Meshes-объектов Бывают случаи, когда можно просто упростить объект, без его восста­ новления (например, модель удаляющейся после выстрела ракеты). Это предполагает, что после упрощения данного объекта мы не сможем улуч­ шить или вернуть его качество. Для примера с ракетой в этом нет необхо­ димости. Но данный пример является далеко не общим случаем. Как пра­ вило, возникает необходимость, когда нам необходимо уменьшить и за­ тем поднять степень детализации объекта. Этого можно достичь, исполь­ зуя более совершенные методы, к которым можно отнести прогрессив­ ные meshes-объекты. Чтобы показать поведение прогрессивных mesh-объектов, мы напи­ шем приложение, опираясь на те методы, которые уже использовали для упрощения объектов, например SimplificationMesh. Однако, вместо того, чтобы только упрощать объект, мы также попробуем поднимать уровень детализации при обратном приближении камеры к объекту. Как и раньше, мы начнем с примера, использующего загрузку объекта из 'файла. Класс ProgressiveMesh происходит от общего класса mesh-объектов BaseMesh. Вы можете использовать класс ProgressiveMesh, объявив его предварительно для вашего объекта:

private ProgressiveMesh progressiveMesh = null;

Необходимо также заменить переменную старого класса Mesh на новую переменную progressiveMesh. Соответственно, для корректной компиляции с новой переменной необходимо переписать метод LoadMesh. Мы будем использовать подобный метод, в конце которого сгенериру­ ем прогрессивный mesh-объект. Используйте код, приведенный в лис­ тинге 9.3.

Часть II. Основные концепции построения графики Листинг 9.3. Загрузка объекта Progressive Mesh private void LoadMesh(string file) { ExtendedMaterial[] mtrl;

GraphicsStream adj;

// Load our mesh using(Mesh mesh = Mesh.FromFile(file, MeshFlags.Managed, device, out adj, 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);

} } )} II Clean our main mesh using(Mesh tempMesh = Mesh.Clean(mesh, adj, adj)) { // Create our progressive mesh progressiveMesh = new ProgressiveMesh(tempMesh, adj, null, 1, MeshFlags.SimplifyVertex);

// Set the initial mesh to the max progressiveMesh.NumberFaces = progressiveMesh.MaxFaces;

progressiveMesh.NumberVertices = progressiveMesh.MaxVertices;

} } } Обратите внимание, что для создания прогрессивного mesh-объекта мы используем два временных объекта: очищенный объект и параметр, необ­ ходимый для конструктора прогрессивного mesh-объекта — минимальное число вершин или сторон, которые мы хотим получить в создаваемом объек­ те, в зависимости от пересылаемого значения MeshFlags (либо упрощение поверхностей Simplify Face, либо упрощение вершин Simplify Vertex). Ее Глава 9. Применение других типов Mesh тественно, это только приближенный подход, но, тем не менее, мы можем получить некоторые преимущества от его использования. Также обратите внимание, что в данном случае мы сразу задаем число вершин и/или сторон в максимальные значения (максимальный уровень детализации), что позволит изменять текущий уровень детализации, ис­ пользуемый при отображении mesh-объекта. Для компиляции приложения необходимо переписать вызов DrawSubset в методе DrawMesh следующим образом:

progressiveMesh.DrawSubset(i);

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

private float cameraPos = 580.Of;

private const int MoveAmount = 100;

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

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

Окончательно, мы должны как обычно обработать операции с клави­ шами, см. листинг 9.4:

Листинг 9.4. Обработчик событий для операции KeyPress. protected override void OnKeyPress(KeyPressEventArgs e) ( if (e.KeyChar == '+') { cameraPos += (MoveAmount * 2 ) ;

progressiveMesh.NumberVertices = ((BaseMesh)progressiveMesh).NumberVertices - MoveAmount;

progressiveMesh.NumberFaces = ((BaseMesh)progressiveMesh).NumberFaces - MoveAmount;

} Часть II. Основные концепции построения графики if (e.KeyChar == '-') { cameraPos -= (MoveAmount * 2);

progressiveMesh.NumberVertices = ((BaseMesh)progressiveMesh).NumberVertices + MoveAmount;

progressiveMesh.NumberFaces = ((BaseMesh)progressiveMesh).NumberFaces + MoveAmount;

} if (e.KeyChar == 'W') device.RenderState.FillMode = FillMode.WireFrame;

if (e.KeyChar == 'S') device.RenderState.FillMode = FillMode.Solid;

} Опять мы выполняем переключение между каркасным и сплошным ре­ жимом отображения, используя клавиши «W» или «S». Для уменьшения уровня детализации и соответствующего перемещения камеры от объекта используется клавиша «+». Обратите внимание, что, прежде чем получить число сторон и вершин, мы должны определить наш объект progressiveMash в классе BaseMesh, чтобы согласовать установку и получение параметров. Легко заметить, что для увеличения уровня детализации и соответ­ ствующего приближения камеры к объекту используется клавиша «-». Таким образом, при приближении камеры к объекту увеличивается чис­ ло отображаемых вершин и сторон. СОХРАНЕНИЕ МНОЖЕСТВЕННЫХ УРОВНЕЙ ДЕТАЛИЗАЦИИ Обычно сохранение множественных объектов с различной степе­ нью детализации проще, чем наличие одного большого прогрессив­ ного mesh-объекта с возможностью управления степенью детали­ зации. Вы можете ознакомиться с прогрессивным mesh-объектом, который включен в поставляемый DirectX SDK, и можете применить методы TrimByFaces и TrimByVertices, чтобы изменять степень де­ тализации объекта. Теперь было бы неплохо добавить к приложению некоторый текст с комментариями о числе отображаемых вершин и сторон. Не будем по­ вторяться с инициализацией и определением шрифта, поскольку данная процедура была подробно описана в этой же главе. Здесь можно лишь добавить следующее: font.DrawText(null, string.Format("Number vertices in mesh: {O}", ((BaseMesh)progressiveMesh).NumberVertices), new Rectangle(l0, 10, 0, 0), DrawTextFormat.NoClip, Color.BlanchedAlmond);

1 ШаыйШШаЙЙНЙШШЙЙНйй*bfirfi tj,ik I ni Глава 9. Применение других типов Mesh font.DrawText(null, string.Format("Number faces in mesh: {0}", ((BaseMesh)progressiveMesh).NumberFaces), new Rectangle(10, 30, 0, 0), DrawTextFormat.NoClip, Color.BlanchedAlmond) ;

Рендеринг патч-объектов. Тесселяция объектов Упрощение объектов достаточно распространенная операция на прак­ тике, но что если ваш объект уже итак слишком прост? С одной стороны, вы имеете достаточную пропускную способность, чтобы отобразить даже большее количество вершин в вашем объекте, чем вы имеете. Но с дру­ гой стороны, вы не можете себе позволить увеличить детализацию уже имеющейся модели, это ничего не даст в плане информационного разре­ шения. Проблему можно решить, применяя патч-объекты. Большинство современных графических ЗD-программ моделирования ориентированы на применение патч-типов mesh или некоторых примитивов старшего разряда, таких, как NURBS (неравномерный рациональный би-сплайн для описания кривых поверхностей) или секционированные/разделен­ ные поверхности. В данной книге мы рассмотрим лишь описание применения патчобъектов, в частности, для увеличения уровня детализации модели. Для этого мы создадим небольшое приложение, позволяющее нам улучшить качество разрешения нашего загружаемого объекта. Снова мы загружаем наш пример из файла (глава 5). Также нам пона­ добится модель для нашего примера, которая находится на CD диске. Программа, находящаяся на CD диске, использует две модели, включен­ ные в DirectX SDK (tiger.x — тигр и cube.x — куб), плюс, маленькую модель сферы, которую вы найдете там же. Убедитесь, что вы копируете текстуру для модели tiger.bmp. Далее нам нужно добавить следующие переменные:

private private private private float tessLevel = l.Of;

const float tesslncrement = l.Of;

string filename = @"..\..\sphere.x";

Microsoft.DirectX.Direct3D.Font font = null;

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

Часть II. Основные концепции построения графики Нам также понадобится внести поправки в метод SetupCamera, на этот раз мы будем использовать несколько меньшие значения удаления каме­ ры от объекта, поскольку имеем меньшие размеры объекта. В соответствии с вышесказанным, запишите метод следующим обра­ зом:

private void SetupCamera() { device.Transform.Projection = Matrix.PerspectiveFovLH( (float)Math.PI / 4, this.Width / this.Height, l.Of, 100.Of);

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

device.Lights[0].Type = LightType.Directional;

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

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

device.Lights[0].Commit () ;

device.Lights[0].Enabled = true;

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

// Create our patch mesh CreatePatchMesh(filename, tessLevel);

// Create our font font = new Microsoft.DirectX.Direct3D.Font(device, new System.Drawing.Font ("Arial", 14.Of, FontStyle.Bold | FontStyle.Italic));

// Default to wireframe mode first device.RenderState.FillMode = FillMode.WireFrame;

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

Глава 9. Применение других типов Mesh Листинг 9.5. Создание патч-объекта mesh. private void CreatePatchMesh(string file, float tessLevel) { if (tessLevel < 1.Of) // Nothing to do return;

if (mesh != null) mesh.Disposed;

using (Mesh tempmesh = LoadMesh(file)) { using (PatchMesh patch = PatchMesh.CreateNPatchMesh(tempmesh)) { // Calculate the new number of faces/vertices int numberFaces = (int)(tempmesh.NumberFaces * Math.Pow(tessLevel, 3));

int numberVerts = (int)(tempmesh.NumberVertices * Math.Pow(tessLevel, 3));

mesh = new Mesh(numberFaces, numberVerts, MeshFlags.Managed MeshFlags.Use32Bit, tempmesh.VertexFormat, device);

// Tessellate the patched mesh patch.Tessellate(tessLevel, mesh);

} } } Если уровень тесселяции, который мы в настоящее время используем, меньше чем l.Of (по умолчанию), ничего в этом методе делать не нужно, и процедура прекращает выполнение. В противном случае мы должны создать новый mesh-объект и применить к нему процедуру тесселляции, сохранив его затем вместо старого. Изменим наш текущий метод LoadMesh, который не возвращает значения данных нормалей, на следу­ ющий, см. листинг 9.6:

Листинг 9.6. Загрузка mesh-объекта с данными нормалей. private Mesh LoadMesh(string file) { ExtendedMaterial[] mtrl;

// Load our mesh 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];

Часть II. О с н о в н ы е концепции построения графики 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);

} } } if ((mesh.VertexFormat & VertexFormats.Normal) != VertexFormats.Normal) { // We must have normals for our patch meshes Mesh tempMesh = mesh.Clone(mesh.Options.Value, mesh.VertexFormat | VertexFormats.Normal, device);

tempMesh.ComputeNormals();

mesh.Dispose();

mesh = tempMesh;

} return mesh;

} Здесь нет ничего такого, чего мы не видели прежде. Если в нашем объекте нет никаких нормалей, мы имитируем новый объект и вычисля­ ем нормали, поскольку они потребуются для тесселяции нашего патчобъекта. И, наконец, мы возвращаем созданный объект (с нормалями). ДИРЕКТИВА USING Обратите внимание, что и для возвращенного объекта, и для патчобъекта, которые мы создаем в этом методе, мы используем клю­ чевое слово «using». Эта директива предполагает автоматическое размещение объекта в классе, если он оставляет область действия оператора. Затем мы создаем новый патч-объект на базе возвращенного объекта. Поскольку мы будем создавать новый патч, подразумевая изменение уров­ ня тесселяции объекта, нам необходимо знать число новых вершин и сто­ рон в этом объекте. После того как мы рассчитали и создали новый объект, мы, наконец, можем его тессилировать. Обратите внимание на использо­ вание флажка MeshFlags.Use32Bit. Поскольку после тесселяции мы мо Глава 9. Применение других типов Mesh жем получить возвращенный объект очень большого размера, необходи­ мо вначале убедиться, что мы можем поддерживать такие объекты. Мы почти готовы к запуску этого приложения. Как и ранее, добавим текст к отображаемой сцене. После вызова метода рисования нашего объекта добавьте следующее:

font.DrawText(null, string.Format ("Number Vertices: {0}\r\nNumber Faces: {1}", mesh.NumberVertices, mesh.NumberFaces), new Rectangle(10,10,0,0), DrawTextFormat.NoClip, Color.Black);

Осталось написать сам метод тесселяции объекта. Мы будем исполь­ зовать код, приведенный в листинге 9.7:

Листинг 9.7. Обработчик событий операции KeyPress для тесселяции объекта. protected override void OnKeyPress(KeyPressEventArgs e) if (e.KeyChar == '+') { tessLevel += tesslncrement;

CreatePatchMesh(filename, tessLevel);

} if (e.KeyChar == '-') { tessLevel -= tesslncrement;

CreatePatchMesh(filename, tessLevel);

} if (e.KeyChar == 'c') { filename = @"..\..\cube.x";

tessLevel = 1.Of;

CreatePatchMesh(filename, tessLevel);

} if (e.KeyChar == 'o') { filename = @"..\..\sphere.x";

tessLevel = l.Of;

CreatePatchMesh(filename, tessLevel);

} if (e.KeyChar == 't') { filename = @".,\. \tiger.x";

tessLevel = l.Of;

Часть II. Основные концепции построения графики CreatePatchMesh(filename, tessLevel);

} if (e.KeyChar == 'W') device.RenderState.FillMode = FillMode.WireFrame;

if (e.KeyChar == 's') device.RenderState.FillMode = FillMode.Solid;

} Как и раньше, для переключения каркасного и сплошного режимов отображения используются клавиши «W» или «S», для изменения уров­ ня тесселяции клавиши «*» и «-». Клавиша «С» используется для того, чтобы отобразить объект «куб», клавиша «Т» — объект «тиф», а клавиша «О» возвращает нас к изобра­ жению сферы.

Примеры тесселированных объектов На рисунках 9.2 — 9.5 приведены примеры отображаемых объектов с использованием различных вариантов тесселяции.

Рис. 9.2. Изображение сферы с маленьким разрешением Изображение сферы (рис.9.2). построенной с помощью треугольни­ ков, выглядит не так реалистично. Однако, если мы разбиваем каждый треугольник на несколько, каче­ ство изображения улучшается (рис.9.3) Глава 9. Применение других типов Mesh Рис. 9.3. Изображение сферы с улучшенным разрешением Рис. 9.4. Изображение тигра с нормальным разрешением Как можно видеть на рис.9.4, изображение тигра выглядит несколько мозаично, хотя использование текстуры определенно помогает скраши­ вать этот недостаток (рис.9.5).

Часть II. Основные концепции построения графики Рис. 9.5. Изображение тигра с высоким разрешением Увеличение параметра тесселяции позволяет значительно улучшить качество картинки.

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

Глава 10. Использование вспомогательных классов Глава 10. Использование вспомогательных классов Рисование линий Прорисовка линий была рассмотрена в главе 4, когда мы имели дело с примитивами типа набора линий или полосок, используемых при ренде­ ринге. Однако, при использовании таких примитивов мы не можем изменять ширину линий, кроме того, объекты, выполненные таким образом, оказываются не достаточно сглаженными. Класс Line может использоваться в зависимости от типа используемо­ го приложения и является достаточно простым и удобным. Рассмотрим пример, в котором попробуем отобразить случайное число линий, непре­ рывно рисуемых на экране. Для этого необходимо создать и подготовить новый проект Direct3D, добавить необходимые ссылки к сборкам, включая класс и using-директиву для списка имен. Так же понадобится создать индивидуальную пе­ ременную для устройства и установить форму окна рендеринга. Эти дей­ ствия мы уже неоднократно проделывали и раньше, поэтому не будем на этом останавливаться. Линии могут быть нарисованы либо в аппаратных координатах уст­ ройства, либо в мировых координатах. В нашем примере мы рассмотрим только экранное пространство, кроме того, мы задействуем буфер глуби­ ны. Используйте метод, приведенный в листинге 10.1, чтобы инициали­ зировать графику.

Листинг 10.1. Инициализация устройства. public void InitializeGraphics{) { // Set our presentation parameters PresentParameters presentParams = new PresentParameters();

presentParams.Windowed = true;

presentParams.SwapEffeet = SwapEffeet.Discard;

// Create our device device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams);

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

• Зак. Часть II. Основные концепции построения графики protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { device.Clear(ClearFlags.Target, Color.Black, l.Of, 0);

device.BeginScene();

//Draw some lines DrawRandomLines();

device.EndScene();

device.Present();

// Let them show for a few seconds System.Threading.Thread.Sleep(250);

this. Invalidated;

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

Листинг 10.2. Рисование произвольно появляющихся линий. private void DrawRandomLines() ( Random r = new Random();

int numberLines = r.Next(50);

using (Line 1 = new Line (device)) { for (int i = 0;

i < numberLines;

i++) { int numVectors = 0;

while (numVectors < 2) numVectors = r.Next(4);

Vector2[] vecs = new Vector2[numVectors];

for(int inner = 0;

inner < vecs.Length;

inner++) { vecs[inner] = new Vector2(r.Next(this.Width), r.Next(this.Height));

} II Color Color с = Color.FromArgb(r.Next(byte.MaxValue), r.Next(byte.MaxValue), r.Next(byte.MaxValue));

int width = 0;

while (width == 0) width = r.Next(this.Width / 100);

// Set the width Глава 10. Использование вспомогательных классов l.Width = width;

// Should they be antialiased? l.Antialias = r.Next(50) > 25 ? true : false;

// Draw the line l.Begin();

l.Draw(vecs, c);

l.End();

} } } Каждый раз при вызове метода сначала определяется случайным об­ разом число линий для рисования, максимум 50 линий. Затем создается отдельный объект line, для прорисовки каждой линии. Возможно также создание одного общего объекта для всех линий. Затем случайным образом выбирается число точек в линии, минимум две, после чего выбирается случайное положение для этих точек (началь­ ной и конечной) с учетом ширины и высоты нашего окна. Также выбира­ ется случайный цвет и ширина линии (также с учетом размеров окна). Режим сглаживания выбирается случайным образом. В зависимости от истинного или ложного значения логической переменной сглаживания, линии будут отображаться соответственно либо «зубчатыми», либо бо­ лее сглаженными. Следует обратить внимание на связку begin/draw/end в листинге 10.2, определяющую рисование линии. Существуют и другие вариан­ ты рисования линии, например, булева переменная GILines, которая определяет, должны ли использоваться линии стиля OpenGL или нет (для выбора по умолчанию устанавливается значение «false»). Можно также использовать метод DrawTransform для рисования линий в трех­ мерном пространстве, а не в используемых здесь аппаратных коорди­ натах. Итак, теперь необходимо переписать основной метод следующим об­ разом:

static void Main() { using (Forml frm = new Forml() ( // Show our form and initialize our graphics engine frm.Show() ;

frm.InitializeGraphics();

Application.Run(frm);

} } Часть II. Основные концепции построения графики При выполнении этого приложения экран заполняется набором бес­ порядочно расположенных линий различных размеров, как гладких, так и «зазубренных». Количество линий на экране будет изменяться случай­ ным образом, как на рис.10.1.

Рис. 1 0. 1. Изображение случайных линий на экране Отображение текста Мы уже обсуждали некоторые вопросы, касающиеся отображения текста на экране, однако, это было довольно поверхностное изучение. Теперь рассмотрим эти вопросы более подробно. Напомним, в предыду­ щих примерах для объекта шрифта Font имелись два пространства имен Microsoft.DirectX.Direct3D и System.Drawing, которые необходимо раз­ личать. Как и раньше, мы можем переименовать используемый класс в соот­ ветствии со следующей директивой «using»:

using Direct3D = Microsoft.DirectX.Direct3D;

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

Глава 10. Использование вспомогательных классов private private private private Direct3D.Font font = null;

Mesh mesh = null;

Material meshMaterial;

float angle = O.Of;

Здесь мы объявляем шрифт и материалы отображаемого текста. В ка­ честве объекта выбирается несложный тесненный текст. Угловой пара­ метр (angle parameter), который мы использовали и ранее, позволит трех­ мерному тексту вращаться со скоростью, зависящей от скорости смены кадров. Теперь необходимо инициализировать графику, используя метод, приведенный в листинге 10.3.

Листинг 10.3. Инициализация графики для рисования фрагмента текста. public void InitializeGraphicsf) i // Set our presentation parameters PresentParameters presentPararas = new PresentParametersO;

presentParams.Windowed = true;

presentParams.SwapEffect = SwapEffeet.Discard;

presentParams.AutoDepthStencilFormat = DepthFormat.D16;

presentParams.EnableAutoDepthStencil = true;

// Create our device device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams);

device.DeviceReset += new System.EventHandler(this.OnDeviceReset);

OnDeviceReset(device, null);

// What font do we want to use? System.Drawing.Font localFont = new System.Drawing.Font ("Arial", 14.Of, FontStyle.Italic);

// Create an extruded version of this font mesh = Mesh.TextFromFont(device, localFont, "Managed DirectX", O.OOlf, 0.4f);

// Create a material for our text mesh meshMaterial = new Material!);

meshMaterial.Diffuse = Color.Peru;

// Create a font we can draw with font = new Direct3D.Font(device, localFont);

Таким образом, мы создаем аппаратное устройство с буфером глуби­ ны, и отслеживаем событие сброса устройства DeviceReset. Поскольку нам необходимо устанавливать освещение и камеру каждый раз после сброса устройства, мы привяжем выполнение этого кода к обработчику событий. Затем с помощью System.Drawing.Font мы задаем шрифт, кото Часть II. Основные концепции построения графики рый будем использовать как основной при рисовании двух- и трехмер­ ных фрагментов тестов. Для данного примера выбран шрифт Arial 14, хотя это не принципиально. Итак, сначала мы создаем трехмерный объект текста в виде теснен­ ной надписи «Managed DirectX», причем создаем отдельный mesh для каждой отображаемой строки. Затем мы определяем материал и цвет ото­ бражаемого 2D-шрифта. РЕНДЕРИНГ ОБЪЕМНОГО ТЕСНЕННОГО ТЕКСТА Рисунок предварительно выбранного двухмерного текста может быть выполнен с помощью двух простых треугольников, тогда как при отображении трехмерного тесненного текста могут понадобить­ ся тысячи треугольников. Как уже упоминалось, для привязки камеры и освещения мы исполь­ зуем обработчик событий, для этого добавляем следующий код: private void OnDeviceReset(object sender, EventArgs e) ( Device dev = (Device)sender;

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

dev.Transform.View = Matrix.LookAtLH(new Vector3(0,0, -9.Of), new Vector3(), new Vector3(0,1,0));

dev.Lights[0].Type = LightType.Directional;

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

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

dev.Lights[0].Commit();

dev.Lights[0].Enabled = true;

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

private void Draw3DText(Vector3 axis, Vector3 location) { device.Transform.World = Matrix.RotationAxis( axis, angle) * Matrix.Translation(location);

device.Material = meshMaterial;

mesh.DrawSubset(O);

Глава 10. Использование вспомогательных классов angle += O.Olf;

} Как вы можете видеть, мы пересылаем координаты mesh-объекта в мировом пространстве и оси, относительно которых будет вращаться текст. Данный метод рисования напоминает метод DrawMesh, который мы использовали в предыдущих примерах, он просто задает материал и рисует первое подмножество тесненного объекта (для этого объекта это будет только одно подмножество). Далее мы увеличиваем угловой пара­ метр. В нашем случае при управлении «анимацией» мы будем привязы­ вать скорость перемещения объекта к частоте смены кадров (как мы уже выясняли, это не совсем обосновано, но пока остановимся на этом). Те­ перь добавим метод рисования 2D-текста:

private void Draw2DText (string text, int x, int y, Color c) { font.DrawText(null, text, new Rectangle(x, y, this.Width, this.Height ), DrawTextFormat.NoClip | DrawTextFormat.ExpandTabs ! DrawTextFormat.WordBreak, c);

} Здесь мы пересылаем текст, который хотим отобразить, и местораспо­ ложение (в аппаратных координатах устройства) верхнего левого угла текста. Последний параметр, естественно, — цвет. Обратите внимание на то, что в нашем запросе к DrawText мы включаем ширину и высоту окна отображения, а также пересылаем флажок WordBreak (перенос стро­ ки). Все это позволяет управлять переходом текста на новую строку или в новое положение, учитывая размер соответствующего ограничиваю­ щего прямоугольника. Теперь мы можем выполнить процедуру рендеринга текстового фраг­ мента, для этого необходимо переписать наш метод OnPaint, см. лис­ тинг 10.4.

Листинг 10.4. Рендеринг фрагмента текста. protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.Black, l.Of, 0);

device.BeginScene();

Draw2DText("Here's some text", 10, 10, Color.WhiteSmoke);

Draw2DText("Here's some text\r\nwith\r\nhard\r\nline breaks", 100, 80, Color.Violet);

Draw2DText("This\tis\tsome\ttext\twith\ttabs.", Часть II. О с н о в н ы е концепции построения графики this.Width / 2, this.Height - 80, Color.RoyalBlue);

Draw2DText("If you type enough words in a single sentence, " + "you may notice that the text begins to wrap. Try resizing " + "the window to notice how the text changes as you size it.", this.Width / 2 + this.Width / 4, this.Height / 4, Color.Yellow);

// Draw our two spinning meshes Draw3DText(new Vector3(l.Of, 1.Of, O.Of), new Vector3(-3.0f, O.Of, O.Of));

Draw3DText(new Vector3(O.Of, l.Of, l.Of), new Vector3(0.0f, -l.Of, l.Of)) ;

device.EndScene();

device.Present ();

this.Invalidated;

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

static void Main() { using (Forml frm = new Forml() { // Show our form and initialize our graphics engine frm.Show() ;

frm.InitializeGraphics() ;

Application.Run(frm);

} } РАСШИРЕННЫЕ ВОЗМОЖНОСТИ ШРИФТА Класс Font рисует текст, основываясь на текстурах, которые созда­ ются в этом классе. Рендеринг этих символов выполняется через графический интерфейс GDI и может быть на самом деле достаточ­ но медленным. Возможно, было бы целесообразнее вызвать два метода предварительной загрузки класса Font в течение запуска, например: PreloadCharacters, чтобы загрузить определенный набор символов, или PreloadText, чтобы загрузить определенную строку. При запуске приложения на экране отобразится текст, который можно увидеть на рис. 10.2. Размеры окна, как и размер текстов, можно изменять.

Глава 10. Использование вспомогательных классов Рис. 10.2. Отображаемый текст Рендеринг на поверхности Если вы когда-нибудь запускали гоночный симулятор, то наверняка помните о возможности использования зеркала заднего обзора. Эти эф­ фекты можно получить, отображая ту же самую сцену (несколько раз с различной привязкой камеры) на имеющуюся текстуру. На первый взгляд это кажется довольно сложным, но на самом деле это вполне реализуе­ мо. Вернемся к примеру из главы 5 и попробуем изменить его должным образом. Естественно, в начале нам необходимо объявить новую тексту­ ру, которую мы будем отображать, плюс, некоторые параметры, которые следует добавить сразу:

private private private private Texture renderTexture = null;

Surface renderSurface = null;

RenderToSurface rts = null;

const int RenderSurfaceSize = 128;

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

Часть II. Основные концепции построения графики device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams);

device.DeviceReset +=new EventHandler(OnDeviceReset);

OnDeviceReset(device, null);

Задание параметров и поддерживаемых устройством опций приведе­ но в листинге 10.5.

Листинг 10.5. Сброс устройства или установка параметров по умолчанию private void OnDeviceReset(object sender, EventArgs e) { Device dev = (Device)sender;

if (dev.DeviceCaps.VertexProcessingCaps.SupportsDirectionalLights) f uint maxLights = (uint)dev.DeviceCaps.MaxActiveLights;

if (maxLights > 0) { dev.Lights[0].Type = LightType.Directional;

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

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

dev.Lights[0].Commit();

dev.Lights[0].Enabled = true;

} if (maxLights > 1) { dev.Lights[l].Type = LightType.Directional;

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

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

dev.Lights[l].Commit();

dev.Lights[l].Enabled = true;

} } rts = new RenderToSurface(dev, RenderSurfaceSize, RenderSurfaceSize, Format.X8R8G8B8, true, DepthFormat.D16);

renderTexture = new Texture(dev, RenderSurfaceSize, RenderSurfaceSize, 1, Usage.RenderTarget, Format.X8R8G8B8, Pool.Default);

renderSurface = renderTexture.GetSurfaceLevel(O);

} На данном этапе мы проверяем, поддерживаются ли в устройстве те или иные возможности. Для начала мы выясняем, поддерживает ли уст­ ройство направленное освещение. Если да, мы включаем его, полагая, что устройство поддерживает несколько источников света. В нашем слу­ чае будем использовать два достаточно ярких источника направленного Глава 10. Использование вспомогательных классов освещения, первый -— для освещения передней стороны нашей модели, второй — для освещения объекта сзади. После создания и включения освещения создается вспомогательный класс RenderToSurface, использующий константы размеров поверхнос­ ти. Обратите внимание, что многие из параметров для этого конструкто­ ра соответствуют параметрам, которые можно найти в представлении устройства. В данном примере используются общие значения, и было бы полезно использовать те же значения из существующей структуры пред­ ставления параметров. Итак, мы создаем нашу текстуру, определяем объект рендеринга и выбираем тот же формат, что и для вспомогательного класса. Все отобра­ жаемые текстуры объекта должны находиться в сброшенном пуле памя­ ти. Получая поверхность из текстуры, мы должны также сохранить и ее. Поскольку мы привязали установку освещения к обработчику собы­ тия сброса, необходимо удалить данную установку из метода SetupCamera, уже существующего в этом примере. Далее нужно добавить новый метод для рендеринга объекта на поверхность, см. метод, приведенный в лис­ тинге 10.6.

Листинг 10.6. Рендеринг на поверхность. private void RenderlntoSurface() f // Render to this surface Viewport view = new Viewport();

view.Width = RenderSurfaceSize;

view.Height = RenderSurfaceSize;

view.MaxZ = l.Of;

rts.BeginScene(renderSurface, view);

device.Clear(ClearFlags.Target ! ClearFlags.ZBuffer, Color.DarkBlue, l.Of, Obdevice.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(angle / (float)Math.PI, angle / (float)Math.PI * 2.Of, angle / (float)Math.PI / 4.Of, O.Of, O.Of, O.Of);

DrawMesh(angle / (float(Math.PI, angle / (float)Math.PI / 2.Of, angle / (float)Math.PI * 4.Of, 150.Of, -100.Of, 175.Of);

rts.EndScene(Filter.None);

) Данный код представляет собой обычный метод рендеринга. Мы вы­ зываем сцену, устанавливаем камеру и рисуем наш объект. Затем мы вы Часть II. Основные концепции построения графики зываем текстуры для нашей сцены и отображаем каждый элемент сцены. В данном случае, помимо обычного расположения камеры, мы еще рас­ полагаем камеру и с другой стороны нашей модели, чтобы мы могли ви­ деть, что происходит, если б находились «сзади» модели. Обратите так­ же внимание, что в этой сцене у нас имеется два варианта отображения: первый, когда мы видим только одну модель (представление спереди, по умолчанию), и второй, когда мы добавляем изображение модели с распо­ ложенной позади объекта камеры (в этом случае мы будем видеть обе модели). Параметром процедуры BeginScene является поверхность, на которую будет осуществлен рендеринг объекта. Поскольку мы используем повер­ хность, восстановленную или полученную из текстуры, любые обновле­ ния этой поверхности будут отражаться и на текстуре. Метод EndScene позволяет использовать текстурную фильтрацию для нашей поверхнос­ ти. В нашем примере мы пока не знаем ничего о поддержке устройством данного фильтра, поэтому фильтрацию опускаем. Последнее, на что сле­ дует обратить внимание, — мы изменили цвет для текстурируемой сце­ ны, для того чтобы показать различие между «реальной» сценой и той, «другой» сценой. Таким образом, мы должны слегка изменить наш метод рендеринга. Для начала необходимо отобразить сцену в нашу текстуру прежде, чем мы начнем отображать ее в нашем основном окне. Добавьте вызов к на­ шему методу в качестве первого пункта в методе OnPaint:

RenderlntoSurfасе();

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

using (Sprite s = new Sprite(device)) ( s.Begin(SpriteFlags.None);

s.Draw(renderTexture, new Rectangle(O, 0, RenderSurfaceSize, RenderSurfaceSize), new Vector3(0, 0, 0), new Vector3(0, 0, l.Of), Color.White);

s.End();

} Этот код просто отображает поверхность всей текстуры в левом верх­ нем углу экрана. Результат рендеринга можно увидеть на рис. 10.3.

Глава 10. Использование вспомогательных классов Рис. 10.3. Рендеринг на поверхность Рендеринг текстур Environment Maps Environment Maps — карты окружающей среды, использующиеся для моделирования рельефных поверхностей, например, при отображении облаков, перемещающихся объектов и пр. Наиболее общий способ для реализации этих карт состоит в том, чтобы использовать кубическую (шестисторонную) текстуру. Прежде чем начать писать соответствующий код, необходимо создать новый проект и выполнить все необходимые приготовления (включая добавление ссылок, переменных устройства, установки окон и пр.). Так­ же понадобятся два объекта (которые включены в DirectX SDK) — мо­ дель автомобиля и модель неба skybox2 (с соответствующими текстурами). После чего объявляются следующие переменные:

private private private private private private private private private private Mesh skybox = null;

Material[] skyboxMaterials;

Texture[] skyboxTextures;

Mesh car = null;

Material[] carMaterials;

Textured carTextures;

CubeTexture environment = null;

RenderToEnvironmentMap rte = null;

const int CubeMapSize = 128;

readonly Matrix ViewMatrix = Matrix.Translation(0.Of, O.Of, 13.Of);

Часть II. Основные концепции построения графики Итак, мы описали два mesh-объекта для рисования: небо (наша окру­ жающая среда) и автомобиль, на который мы хотим «отражать» окружа­ ющую среду. Также необходимо иметь кубическую текстуру, которая со­ хранит среду, и вспогательный класс для отображения карты среды. Но, прежде всего, мы должны выяснить, поддерживает ли наша карта кубические текстуры или нет. Поэтому нам нужен новый графический метод инициализации нашего устройства, см. листинг 10.7.

Листинг 10.7. Инициализация устройства для отображения карт public bool InitializeGraphicsO ( // Set our presentation parameters PresentParameters presentParams = new PresentParametersO;

presentParams.Windowed = true;

presentParams.SwapEffeet = SwapEffeet.Discard;

presentParams.AutoDepthStencilFormat = DepthFormat.D16;

presentParams.EnableAutoDepthStencil = true;

// Create our device device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams);

device.DeviceReset +=new EventHandler(OnDeviceReset);

OnDeviceReset(device, null);

// Do we support cube maps? if (!device.DeviceCaps.TextureCaps.SupportsCubeMap) return false;

// Load our meshes skybox = LoadMesh(@"..\..\skybox2.x", ref skyboxMaterials, ref skyboxTextures);

car = LoadMesh(@".,\..\car.x", ref carMaterials, ref carlextures) return true;

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

static void Main() { using (Forml frm = new Forml()) ( // Show our form and initialize our graphics engine frm.Show();

Глава 10. Использование вспомогательных классов if (lfrm.InitializeGraphi.es ()) { MessageBox.Show("Your card does not support cube maps.");

frm. Closed;

} else { Application.Run(frm);

} } } Как видно, здесь нет ничего необычного. Рассмотрим метод, загружа­ ющий наш объект (подобно тому, что мы уже делали раньше). После заг­ рузки объекта, материалов и текстур, метод проверяет наличие данных о нормалях (если нет, то добаляет их) и затем возвращает полученный объект. Используйте код, приведенный в листинге 10.8.

Листинг 10.8. Загрузка объекта с данными нормалей. private Mesh LoadMesh(string file,ref Material[] meshMaterials,ref Texture[] meshTextures) { ExtendedMaterial[] mtrl;

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

// If we have any materials, store them if ((mtrl != null) SS (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) 4& (mtrl[i].TextureFilename != string.Empty)) { / / W e have a texture, try to load it meshTextures[i] = TextureLoader.FromFile(device, @"..\..\" + mtrl[i].TextureFilename);

} } } if ((mesh.VertexFormat & VertexFormats.Normal) != VertexFormats.Normal) { Часть II. Основные концепции построения графики // We must have normals for our patch meshes Mesh tempMesh = mesh.Clone(mesh.Options.Value, mesh.VertexFormat | VertexFormats.Normal, device);

tempMesh.ComputeNormals();

mesh.Disposed;

mesh = tempMesh;

} return mesh;

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

private void OnDeviceReset(object sender, EventArgs e) ( Device dev = (Device)sender;

rte = new RenderToEnvironmentMapjdev, CubeMapSize, 1, Format.X8R8G8B8, true, DepthFormat.D16);

environment = new CubeTexture(dev, CubeMapSize, 1, Usage.RenderTarget, Format.X8R8G8B8, Pool.Default);

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

Листинг 10.9. Рендеринг карты окружающей сцены. private void RenderScenelntoEnvMap() { // Set the projection matrix for a field of view of 90 degrees Matrix matProj;

matProj = Matrix.PerspectiveFovLH((float)Math.PI * 0.5f, l.Of, 0.5f, 1000.Of);

// Get the current view matrix, to concat it with the cubemap view vectors Глава 10. Использование вспомогательных классов Matrix matViewDir = ViewMatrix;

matViewDir.M41 = O.Of;

matViewDir.M42 = O.Of;

matViewDir.M43 = O.Of;

// Render the six cube faces into the environment map if (environment != null) rte.BeginCube(environment);

for (int i = 0;

i < 6;

i++) { rte.Facef(CubeMapFace) i, 1);

// Set the view transform for this cubemap surface Matrix matView = Matrix.Multiply(matViewDir, GetCubeMapViewMatrix((CubeMapFace) i));

// Render the scene (except for the teapot) RenderScene(matView, matProj, false);

} rte.End(l);

) Итак, сначала мы создаем матрицу проекции с полем зрения 90 граду­ сов (соответствует углам между гранями куба). Затем получаем матрицу вида, которую мы сохранили для этого приложения и изменяем после­ днюю строку матрицы таким образом, чтобы можно было объединить ее с матрицами вида для каждой стороны куба. Далее мы вызываем метод BeginCube, принадлежащий вспомогатель­ ному классу, для создания кубической карты окружающей среды. В этом классе имеются и другие методы создания карт среды, включая BeginHemisphere и BeginParabolic (использующие две поверхности, одна для положительной ветви Z, другая для отрицательной), а также BeginSphere (использующий единственную поверхность). Когда карта среды готова для отображения, мы запускаем цикл для каждой стороны куба. Далее для каждой стороны мы вначале вызываем метод Face, аналогичный методу BeginScene, который мы использовали для нашего устройства, и текстурные рендеры. Метод «отслеживает» со­ бытие, когда последняя сторона закончена, а новая готова к отображе­ нию. Затем мы объединяем текущую матрицу вида с теми, которые были получены в методе GetCubeMapViewMatrix. Данный метод приведен в листинге 10.10.

Листинг 10.10. Создание матрицы вида, ViewMatrix. private Matrix GetCubeMapViewMatrix(CubeMapFace face) { Vector3 vEyePt = new Vector3(0.0f, O.Of, O.Of);

Vector3 vLookDir = new Vector3();

Vector3 vUpDir = new Vector3();

switch (face) Часть II. Основные концепции построения графики case CubeMapFace.PositiveX: vLookDir = new Vector3(1.0f, O.Of, O.Of);

vUpDir = new Vector3(O.Of, l.Of, O.Of);

break;

case CubeMapFace.NegativeX: vLookDir = new Vector3(-1.0f, O.Of, O.Of);

vUpDir = new Vector3(0.0f, l.Of, O.Of);

break;

case CubeMapFace.PositiveY: vLookDir = new Vector3(0.0f, l.Of, O.Of);

vUpDir = new Vector3(O.Of, O.Of,-l.Of);

break;

case CubeMapFace.NegativeY: vLookDir = new Vector3(O.Of,-l.Of, O.Of);

vUpDir = new Vector3(O.Of, O.Of, l.Of);

break;

case CubeMapFace.PositiveZ: vLookDir = new Vector3(0.0f, O.Of, l.Of);

vUpDir = new Vector3(0.0f, l.Of, O.Of);

break;

case CubeMapFace.NegativeZ: vLookDir = new Vector3(0.0f, 0.Of,-l.Of);

vUpDir = new Vector3(0.0f, l.Of, O.Of);

break;

} // Set the view transform for this cubemap surface Matrix matView = Matrix.LookAtLH(vEyePt, vLookDir, vUpDir);

return matView;

} Здесь мы просто изменяем параметры вида в зависимости от того, на какую сторону мы смотрим, и возвращаем матрицу вида, составленную на основе этих векторов. После всех этих действий мы отображаем всю сцену без освещенного солнцем автомобиля (параметр «false» в методе RenderScene). Добавьте код, приведенный в листинге 10.11.

Листинг 10.11. Рендеринг всей сцены. private void RenderScene(Matrix View, Matrix Project, bool shouldRenderCar) { // Render the skybox first device.Transform.World = Matrix.Scaling(10.Of, 10.Of, lO.Of);

Matrix matView = View;

matView.M41 = matView.M42 = matView.M43 = O.Of;

device.Transform.View = matView;

device.Transform.Projection = Project;

Глава 10. Использование вспомогательных классов device.TextureState[0].ColorArgumentl = TextureArgument.TextureColor;

device,TextureStateiO].ColorOperation = TextureOperation.SelectArgl;

device.SamplerState[0].MinFilter = TextureFilter.Linear;

device.SamplerState[0].MagFilter = TextureFilter.Linear;

device.SamplerState[0].AddressU = TextureAddress.Mirror;

device.SamplerState[0].AddressV = TextureAddress.Mirror;

// Always pass Z-test, so we can avoid clearing color and depth buffers device.RenderState.ZBufferFunction = Compare.Always;

DrawSkyBoxO;

device.RenderState.ZBufferFunction = Compare.LessEqual;

// Render the shiny car if (shouldRenderCar) ( // Render the car device.Transform.View = View;

device.Transform.Projection = Project;

using (VertexBuffer vb = car.VertexBuffer) { using (IndexBuffer ib = car.IndexBuffer) ( // Set the stream source device.SetStreamSource(0, vb, 0, VertexInformation.GetFormatSize(car.VertexFormat));

// And the vertex format and indices device.VertexFormat = car.VertexFormat;

device.Indices = ib;

device.SetTexture(0, environment);

device.SamplerState[0].MinFilter = TextureFilter.Linear;

device.SamplerState[0].MagFilter = TextureFilter.Linear;

device.SamplerState[0].AddressQ = TextureAddress.Clamp;

device.SamplerState[0].AddressV = TextureAddress.Clamp;

device.SamplerState[0].AddressW = TextureAddress.Clamp;

device.TextureState[0].ColorOperation = TextureOperation.SelectArgl;

device.TextureState[0].ColorArgumentl = TextureArgument.TextureColor;

device.TextureState[0].TextureCoordinatelndex = (int)TextureCoordinatelndex.CameraSpaceReflectionVector;

device. TextureState[0].TextureTransform = TextureTransform.Count3;

device.Transform.World = Matrix.RotationYawPitchRoll( angle / (float)Math.PI, angle / (float)Math.PI * 2.Of, angle / (float)Math.PI / 4.0f);

angle += O.Olf;

device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, car.NumberVertices, 0, car.NumberFaces);

} } } } Часть II. Основные концепции построения графики Первое, что мы здесь делаем, — отображаем объект skybox. Посколь­ ку мы знаем, что размер skybox меньше, чем нам необходимо, мы увели­ чиваем его в 10 раз. Затем мы задаем текстуру и состояние сэмплера, который необходим для рендеринга текстуры skybox (с этим можно оз­ накомиться в документах MSDN). Если вы обратили внимание, мы никогда не очищали устройство в наших примерах. Поскольку нам бы не хотелось вызывать эту процеду­ ру при отображении каждой из сторон куба, мы выполняем ее до ото­ бражения карты среды skybox (которая, как мы знаем, будет иметь са­ мое большое значение глубины в нашей сцене) и устанавливаем состо­ яние рендера таким образом, чтобы буфер глубины был всегда досту­ пен во время отображения текстуры skybox. С другой стороны, мы ото­ бражаем текстуру skybox и переключаем функцию буфера глубины на­ зад к нормали. Выполнение метода Skybox должно показаться знако­ мым для нас: private void DrawSkyBoxO ( for (int i = 0;

i < skyboxMaterials.Length;

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

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

skybox.DrawSubset(i) ;

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

device.TextureState[0].TextureCoordinatelndex = (int)TextureCoordinatelndex.CameraSpaceReflectionVector;

device.TextureState[0].TextureTransform = TextureTransform.Count3;

Глава 10. Использование вспомогательных классов Так как мы не имеем координат текстуры объекта автомобиль, первая строка подразумевает использование вектора отражения (преобразован­ ного в пространстве камеры) вместо использования координат текстур. Значения вектора автоматически сгенерированы в непрограммируемом конвейере fixedfunction pipeline, использующем данные местоположения вершины и нормали к нашей ЗD-текстуре. Теперь мы можем преобразовать координаты и нарисовать наши при­ митивы. Приложение практически готово к выполнению, но сначала мы должны добавить наш метод рендеринга:

protected override void 0nPaint(System.Windows.Forms.PaintEventArgs e) [ RenderScenelntoEnvMap();

device.BeginSceneO;

RenderScene(ViewMatrix, Matrix.PerspectiveFovLHf(float)Math.PI / 4, this.Width / this.Height, l.Of, 10000.Of), true);

device. EndSceneO;

device.Present!);

this.Invalidate!);

I Для каждого рендера мы сначала отображаем карту окружающей сре­ ды и уже затем отображаем нашу «реальную» сцену, включая автомобиль. Приложение готово к запуску. В результате мы должны увидеть на экране освещенный солнцем автомобиль, вращающийся на фоне неба, рис. 10.4.

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

ЧАСТЬ III БОЛЕЕ СОВЕРШЕННЫЕ МЕТОДЫ ПОСТРОЕНИЯ ГРАФИКИ Глава 11. Введение в программируемый конвейер, язык шейдеров Глава 12. Использование языка шейдеров HLSL Глава 13. Рендеринг скелетной анимации Часть III. Более совершенные методы построения графики Глава 1 1. Введение в программируемый конвейер, язык шеидеров До этого момента для процедуры рендеринга мы использовали не­ программируемый конвейер. До создания DirectX 8.0 это было единствен­ ным способом отображать что-либо на экране. Непрограммируемый кон­ вейер является по существу набором правил и параметров, которые уп­ равляют представленными типами данных. Тем не менее, его использо­ вание не позволяет реализовать многие возможности процедур и опций рендеринга, например, при использовании освещения. С появлением DirectX 8.0 появилась возможность работать с програм­ мируемым конвейером, которая позволила разработчикам управлять па­ раметрами конвейера. Все это привело к появлению шеидеров или языка построения теней в компьютерной графике. Теперь разработчики могли управлять обработкой вершин с помощью вершинных шеидеров и обработкой пикселей с помощью пиксельных шеи­ деров. Программы, использующие язык шеидеров оказались весьма мощ­ ными, но на тот момент не совсем удобными. Синтаксис языка напоми­ нал ассемблирование и требовал некоторых доработок и упрощений. В Управляемом DirectX 9 был реализован язык HLSL — язык шеиде­ ров высокого уровня. Язык HLSL был более простым для разработчиков и напоминал язык С, который мог быть откомпилирован в реальный код шейдера. Таким образом, могли быть реализованы все преимущества программируемого конвейера в более доступной и удобной форме. В этой главе мы рассмотрим основные особенности языка шеидеров HLSL, вклю­ чая. Использование программируемого конвейера. Преобразование вершин с помощью шеидеров. • Использование пиксельных шеидеров.

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

Глава 11. Введение в программируемый конвейер, язык шейдеров Одним из главных недостатков такого подхода являлось то, что для каждого отдельного свойства или возможности, которую графическая карта должна реализовать, необходимо было использовать непрограм­ мируемый API интерфейс. С быстрым развитием аппаратной части ком­ пьютерной графики (даже более быстрым, чем, например, процессоров), число интерфейсов API быстро бы вышло из-под контроля. Кроме того, не было возможности управлять интерфейсами, что весьма необходимо для разработчиков компьютерной графики (особенно разработчиков игр). Если вспомнить, одним из первых приложений, рассмотренных в этой книге, было отображение вращающегося треугольника. Это было весьма просто сделать с использованием непрограммируемого конвейера. Теперь для этого примера мы будем использовать программируемый конвейер. Как обычно, вначале создаем новые формы windows-приложения, объяв­ ляем переменную устройства, стиль окна и т. д. Для этого добавим сле­ дующие переменные:

private VertexBuffer vb = null;

private Effect effect = null;

private VertexDeclaration decl = null;

// Our matrices private Matrix worldMatrix;

private Matrix viewMatrix;

private Matrix projMatrix;

private float angle = 0. Of;

Естественно, мы будем записывать данные вершин в вершинном бу­ фере. Необходимо также сохранить матрицы преобразования до того момента, когда мы будем использовать их в программируемом конвей­ ере. Вторая и третья переменные, описанные здесь, — новые для нас. Effect object— главный объект, который вы будете использовать при программировании на языке шейдеров HLSL. Класс объявления вер­ шин VertexDeclaration подобен вершинному формату в непрограмми­ руемом конвейере. Данный класс сообщает приложению Direct3D runtime о размере и типах данных, которые будут считываться из вер­ шинного буфера. Поскольку это приложение будет использовать относительно новую возможность графических карт, а именно программируемый конвейер, вполне возможно, что ваша графическая плата не будет поддерживать эту опцию. Если это так, необходимо подключить ссылки на эмулиро­ ванное устройство, которое включено в DirectX SDK, другим словами, создать программную эмуляцию устройства. Для данного случая работа API будет выполняться несколько медленнее, и возникает необходимость в использовании более быстрой процедуры инициализации нашего уст­ ройства, см. листинг 11.1.

Часть III. Более совершенные методы построения графики Листинг 11.1. Инициализация графики, проверка поддерживаемых режимов (Fallback). public bool { InitializeGraphics() // Set our presentation parameters PresentParameters presentParams = new PresentParameters();

presentParams.Windowed = true;

presentParams.SwapEffeet = SwapEffeet.Discard;

presentParams.AutoDepthStencilFormat = DepthFormat.D16;

presentParams.EnableAutoDepthStencil = true;

bool canDoShaders = true;

// Does a hardware device support shaders? Caps hardware = Manager.GetDeviceCapsfO, DeviceType.Hardware);

if (hardware.VertexShaderVersion >= new Version(1, 1)) { // Default to software processing CreateFlags flags = CreateFlags.SoftwareVertexProcessing;

// Use hardware if it's available if (hardware.DeviceCaps.SupportsHardwareTransformAndLight) flags = CreateFlags.HardwareVertexProcessing;

// Use pure if it's available if (hardware.DeviceCaps.SupportsPureDevice) flags |= CreateFlags.PureDevice;

// Yes, Create our device device = new Device(0, DeviceType.Hardware, this, flags, presentParamss :

} else { / / N o shader support canDoShaders = false;

// Create a reference device device = new Device(0, DeviceType.Reference, this, CreateFlags.SoftwareVertexProcessing, presentParams);

} // Create our vertex data vb = new VertexBuffer(typeof(CustomVertex.PositionOnly), 3, device, Usage.Dynamic | Usage.WriteOnly, CustomVertex.PositionOnly.Format, Pool.Default);

vb.Created += new EventHandler(this.OnVertexBufferCreate);

OnVertexBufferCreate(vb, null);

// Store our project and view matrices projMatrix = Matrix.PerspectiveFovLH((float)Math.PI / 4, this.Width / this.Height, l.Of, 100.Of);

viewMatrix = Matrix.LookAtLH(new Vector3(0,0, 5.Of), new Vector3(), new Vector3(0,1,0));

// Create our vertex declaration Глава 11. Введение в программируемый конвейер, язык шейдеров VertexElement[] elements = new VertexElement[] ( new VertexElement(0, 0, DeclarationType.Float3, DeclarationMethod.Default, DeclarationUsage.Position, 0), VertexElement.VertexDeclarationEnd I;

decl = new VertexDeclaration(device, elements);

return canDoShaders;

} Данный код предполагает использование для рендеринга сцены адап­ тера, выбранного по умолчанию (нулевое значение параметра). При этом, здесь опущена процедура перебора, вместо этого, как уже сказано, выб­ ран адаптер, используемый по умолчанию. Вы можете при необходимос­ ти изменить код в соответствии с используемой графической картой. Перед созданием устройства, мы, как и раньше, проверяем поддержива­ емые им возможности, если вспомнить, данная структура называлась Caps (возможности). В настоящем приложении, поскольку здесь мы используем для ренде­ ринга вершин программируемый конвейер, необходимо убедиться, что наша карта поддерживает по крайней мере шейдеры первого поколения. С появлением новых версий API версии вершинных и пиксельных шей­ деров также обновлялись. Например, DirectX 9 позволяет нам использо­ вать шейдеры версии 3.0 и старше (хотя в настоящее время нет подходя­ щих графических карт, поддерживающих, к примеру, третью версию). Первое поколение шейдеров имело версию 1.0. В DirectX 9 эта устарев­ шая версия была заменена версией 1.1, и это то, с чем мы здесь будем работать. Предполагая, что карта поддерживает эту версию, мы можем создать «более продвинутый» тип устройства. Мы задаем по умолчанию про­ граммную обработку вершин, а если устройство поддерживает и аппа­ ратную обработку, будем использовать ее. В случае, если плата не под­ держивает программы шейдеров, нам ничего не остается, кроме как ис­ пользовать программное устройство — эмулятор, которое поддерживает режим runtime, хотя это будет намного медленнее. Далее необходимо создать вершинный буфер для нашего отображае­ мого треугольника. Обработчик событий в этом случае будет иметь сле­ дующий вид:

private void OnVertexBufferCreate(object sender, EventArgs e) { VertexBuffer buffer = (VertexBuffer)sender;

CustomVertex.PositionOnly[] verts = new CustomVertex.Position0nly[3];

Часть III. Более совершенные методы построения графики verts[0].SetPosition(new Vector3(0.0f, l.Of, l.Of));

verts[l].SetPosition(new Vector3(-1.0f, -l.Of, l.Of));

verts[2].SetPosition(new Vector3(1.0f, -l.Of, l.Of));

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

} После создания вершинного буфера необходимо сохранить результа­ ты преобразования координат для дальнейшего использования. Данные преобразования выполняются тем же самым способом, что и раньше, с единственным отличием — теперь для записи этих данных мы не будем использовать свойство преобразования устройства. Итак, мы дошли до процедуры объявления вершин, «сообщающей» Direct3D всю необходимую информацию о вершинах, которая будет пере­ сылаться в программируемый конвейер. При создании объекта класса • VertexDeclaration в используемое устройство пересылается массив вершин­ ных элементов, каждый из которых описывает один компонент данных вершин. Рассмотрим следующий конструктор для элемента вершины:

public VertexElement ( System.Intl6 stream, Microsoft.DirectX.Direct3D.DeclarationType Microsoft.DirectX.Direct3D.DeclarationMethod Microsoft.DirectX.Direct3D.DeclarationUsage System.Byte usagelndex ) System.Intl6 offset, declType, declMetftod, declUsage, Первый параметр stream — поток данных о вершинах. Когда мы вы­ зывали метод SetStreamSource, этот параметр представлял собой поток, пересылаемый в вершинный буфер. До сих пор мы хранили все данные в одном вершинном буфере, используя один поток;

теперь мы можем про­ водить рендеринг объекта, используя данные из различных вершинных буферов с различными потоками данных. В данном примере, поскольку мы имеем только один вершинный буфер (поток с нулевым порядковым номером), мы задаем нулевое значение для этого параметра. Второй параметр offset — смещение в буфере, где хранятся данные. Поскольку в нашем случае имеется только один тип данных вершинного буфера, данное значение также будет нулевым. При использовании не­ скольких типов данных значение смещения задается для каждого. На­ пример, если первый компонент — местоположение вершины (три зна­ чения с плавающей точкой), а второй компонент — нормаль (также три значения с плавающей точкой), тогда первый компонент будет иметь сме­ щение «О» (поскольку это первый компонент в буфере), тогда как нор­ маль — смещение «12» (четыре байта на каждую цифру). Третий параметр declType сообщает приложению Direct3D тип ис­ пользуемых данных. Так как в этом примере мы используем только ме­ стоположение вершины, можно использовать тип Float3 (этот тип мы обсудим позже).

Глава 11. Введение в программируемый конвейер, язык шейдеров Четвертый параметр declMethod описывает используемый метод. В большинстве случаев (если не используются примитивы более высокого порядка), метод устанавливается по умолчанию. Пятый параметр declUsage описывает использование компонентов, таких как: местоположение, нормаль, цвета и пр. Для описания местопо­ ложения используются три числа с плавающей запятой. Последний параметр изменяет данные использования, что позволяет определять множественные типы использования. В большинстве случа­ ев это нулевое значение. Важно обратить внимание, что ваш массив элементов вершины дол­ жен иметь в качестве последнего значения запись VertexElement.VertexDeclarationEnd. Так, для простого объявления вершины можно использо­ вать поток с нулевым значением номера, состоящий из трех чисел с пла­ вающей запятой, представляющих месторасположения вершины. После создания массива элементов вершин можно создавать объект объявления вершины. Окончательно, программа возвращает булеву переменную, имеющую значение «true», если аппаратное устройство поддерживает шейдеры, и значение «false», если используется эмулированное устрой­ ство. Таким образом, необходимо вызвать метод инициализации, использу­ ющий эту булеву переменную. Перепишите метод следующим образом:

static void Main() { using (Forml frm = new Forml()) { // Show our form and initialize our graphics engine frm.Show();

if (Ifrra.InitializeGraphics()) { MessageBox.Show("Your card does not support shaders. " + "This application will run in ref mode instead.");

} Application.Run(frm);

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

protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.CornflowerBlue, l.Of, 0);

Часть III. Более совершенные методы построения графики UpdateWorldO;

device.BeginScene ();

device.SetStreamSource(0, vb, 0);

device.VertexDeclaration = decl;

device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);

device.EndScenef);

device.Present ();

this.Invalidate));

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

private void UpdateWorld() { worldMatrix = Matrix.RotationAxis(new Vector3(angle / ((float)Math.PI * 2.Of), angle / ((float)Math.PI * 4.Of), angle / ((float)Math.PI * 6.0f)), angle / (float)Math.PI);

angle += O.lf;

) Достаточно знакомый код, за исключением того, что мы устанавлива­ ем свойство объявления вершины vertex declaration вместо свойства фор­ мата вершины vertex format. Выполняя приложение в том виде, каком оно есть на данный момент, мы ничего не увидим кроме синего экрана. С чем это может быть связано? Все достаточно просто, приложение Direct3D runtime понятия не имеет, что вы хотите сделать. Мы должны написать реальную «программу» для программируемого конвейера. Итак, добавьте новый пустой текстовый файл simple.fx к вашему про­ екту. В этот файл мы будем сохранять программу, написанную на языке HLSL. Теперь необходимо добавить следующий HLSL код в этом файле:

// Shader output, position and diffuse color struct VS_0UTPUT { float4 pos : POSITION;

float4 diff : COLOR0;

};

// The world view and projection matrices float4x4 HorldViewProj : WORLDVIEWPROJECTION;

float Time = l.Of;

// Transform our coordinates into world space VS OUTPUT Transform) Глава 11. Введение в программируемый конвейер, язык шейдеров float4 Pos : POSITION) { // Declare our return variable VS_0UTPUT Out = (VS_OUTPUT)0;

// Transform our position Out.pos = mul(Pos, WorldViewProj);

// Set our color Out.diff.r = 1 - Time;

Out.diff.b = Time * WorldViewProj[2].yz;

Out.diff.ga = Time * WorldViewProj[0].xy;

// Return return Out;

I Легко заметить, что код HLSL напоминает язык С (и С#). Вначале мы объявляем структуру, которая будет связывать выходные данные вершин. Процедура объявления переменных несколько отличается от привычного нам алгоритма. К каждой переменной была добавлена семантика (другими сло­ вами, тэги, указывающие на переменную класса usage: в нашем случае это местоположение и первоначальный цвет, соответственно position и color). Возникает вопрос, почему в структуре вывода находятся и местопо­ ложение, и цвет, тогда как вершинный буфер содержит данные только о местоположении? Поскольку структура вывода содержит и местополо­ жение, и цвет (вместе с соответствующей семантикой), Direct3D будет «знать» что рендерингу подлежат новые данные, возвращенные из про­ граммы обработки вершин или вершинных шейдеров, вместо данных, сохраненных в вершинном буфере. Далее, имеются две «глобальные» переменные, затрагивающие раз­ личные преобразования: первая — комбинацию матрицы вида, проекции и пр., и вторая — время анимирования цвета треугольника. Во время выполнения приложения необходимо обновлять или перезаписывать все эти параметры для каждого кадра. Итак, у нас имеется реальный код вершинного шейдера. Как можно видеть, он возвращает структуру, которую мы уже создали, и принимает в качестве параметра положение вершин. Этот метод будет вызываться для каждой из вершин, находящихся в вершинном буфере (в нашем слу­ чае трех). Обратите внимание, что входная переменная также имеет опи­ сание, указывающее приложению Direct3D тип используемых данных. Сам по себе код внутри приведенного метода весьма прост. Мы объяв­ ляем возвращаемую переменную Out. Далее выполняется процедура пре­ образования, при котором начальное значение местоположения умножа­ ется на сохраненную матрицу преобразования. Здесь используется встро­ енная функция mull, причем ранг матрицы преобразования — float4x4, a вектора местоположения — float4. Необходимо строго согласовывать форматы при перемножении в соответствии с известными операциями Часть III. Более совершенные методы построения графики над матрицами, в противном случае, оператор не выполнит данную опе­ рацию из-за несоответствия типов. После этого мы определяем цвета, используя простую формулу для каждого компонента цвета. Сначала устанавливается красный, затем си­ ний и в конце зеленый. Таким образом, установив значения расположе­ ния и цвета, мы имеем заполненную структуру, которая будет использо­ ваться в дальнейших операциях. ОБЪЯВЛЕНИЕ ПЕРЕМЕННЫХ В ЯЗЫКЕ HLSL И ВСТРОЕННЫЕ ТИПЫ Обратите внимание на то, что мы используем некоторые встроен­ ные типы, которых нет в языках С или С#, а именно float4 и float4x4. Следует отметить, что язык шейдеров поддерживает следующие скалярные типы. Bool — булева переменная, «true» или «false». Int — 32-разрядное целое число со знаком. Half— 16-разрядное число с плавающей запятой. Float — 32-разрядное число с плавающей запятой. Double — 64-разрядное число с плавающей запятой. Объявление переменных этих типов просходит также, как и в языке С#. Однако, добавление отдельного целого числа в конце одного из этих скалярных типов объявит «векторный» тип. Векторные типы могут использоваться как векторы или массивы. Например:

float4 pos;

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

float2 someVector;

Вы можете обращаться к значениям этой переменной как к массиву:

pos[0] = 2.Of;

Также можно обращаться к этому типу аналогичным образом, что и к вектору в С#:

pos.x = 2.Of;

В этом случае мы можем обращаться к одному или к нескольким ком­ понентам вектора. Это называется «swizzling» или обращение по ад­ ресам. Мы можем использовать компоненты (x,y,z,w) вектора, также Глава 11. Введение в программируемый конвейер, язык шейдеров как составляющие цвета (r,g,b,a);

однако, мы не можем смешивать эти компоненты в данном процессе. Пример корректной записи: pos.xz = O.Of;

pos.rg += pos.xz;

Пример некорректной записи: pos.xg = O.Of;

Pages:     | 1 | 2 || 4 | 5 |   ...   | 6 |



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

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