netlib.narod.ru | < Назад | Оглавление | Далее > |
В Direct3D есть механизм, необходимый для размещения данных вершин в памяти видеокарты: буферы вершин. Буфер вершин (vertex buffer), согласно своему названию, представляет область памяти, используемую для хранения вершин. Гибкость буферов врешин делает их идеальными для совместного использования преобразуемой геометрии вашей сцены. Так как мы можем преобразовать рисующее треугольник приложение из главы 1 для использования буфера вершин?
Создать буфер вершин очень просто. Для этого можно использовать три конструктора, и мы рассмотрим каждый из них.
public VertexBuffer (Microsoft.DirectX.Direct3D.Device device, System.Int32 sizeOfBufferInBytes, Microsoft.DirectX.Direct3D.Usage usage, Microsoft.DirectX.Direct3D.VertexFormats vertexFormat, Microsoft.DirectX.Direct3D.Pool pool) public VertexBuffer (System.Type typeVertexType, System.Int32 numVerts, Microsoft.DirectX.Direct3D.Device device, Microsoft.DirectX.Direct3D.Usage usage, Microsoft.DirectX.Direct3D.VertexFormats vertexFormat, Microsoft.DirectX.Direct3D.Pool pool)
Опишем назначение параметров:
device — устройство Direct3D, используемое для создания буфера. Буфер вершин можно использовать только для этого устройства.
SizeOfBufferInBytes — Размер создаваемого буфера вершин в байтах. Если вы используете конструктор с этим параметром, буфер может содержать вершины любого типа.
typeVertexType — Если вы хотите, чтобы ваш буфер вершин мог содержать вершины только одного типа, то указываете этот тип здесь. Это может быть одна из встроенных структур данных вершин класса CustomVertex, либо определяемый пользователем тип вершин. Значение параметра не может быть null.
numVerts — Если вы задали тип вершин, которые хотите хранить в буфере, то необходимо также указать максимальное количество вершин данного типа, которое сможет хранить буфер. Значение должно быть больше нуля.
usage — определяет, как будет использоваться буфер вершин. Не все члены типа Usage могут использоваться при создании буфера вершин. Допустимы следующие значения:
DoNotClip — используется для указания того, что данный буфер вершин никогда не требует отсечения. При визуализации буфера вершин, использующего этот флаг, необходимо установить false для режима отсечения.
Dynamic — Используется для указания того, что данный буфер требует применения динамической памяти. Если флаг отсутствует, буфер вершин будет статическим. Статические вуферы вершин обычно хранятся в памяти видеокарты, а динамические буферы — в памяти AGP, так что выбор этого параметра помогает драйверу определить, какую область памяти использовать для хранения. Более подробные сведения об использовании параметра Usage вы найдете в документации к DirectX SDK.
Npatches — Используется для указания того, что данный буфер вершин применяется для рисования N-патчей (N-patches, трехмерные поверхности, основанные на B-сплайнах).
Points — Сообщает, что данный буфер вершин используется для рисования точек.
RTPatches — Указывает, что буфер вершин используется для рисования примитивов высокого порядка.
SoftwareProcessing — Используется для указания того, что обработка вершин должна выполняться программным обеспечением. Если флаг не указан, обработка вершин будет выполняться аппаратурой.
WriteOnly — сообщает, что чтение из этого буфера вершин выполняться не будет. Если у вас нет очень серьезных причин для чтения данных из буфера вершин, всегда следует выбирать этот флаг. Если сохранять данные в видеопамяти без этого флага, быстродействие будет падать.
vertexFormat — определяет формат вершин, которые будут храниться в данном буфере. Если вы хотите создать буфер общего назначения, выберите значение VertexFormat.None.
pool — Определяет пул памяти, в котором будет располагаться буфер вершин. Вы можете задать один из следующих вариантов:
Default — Буфер вершин будет размещен в памяти, наиболее подходящей для данных, которые он содержит. Обычно это либо память видеокарты, либо память AGP, в зависимости от значения параметра usage. Буферы вершин, создаваемые в этом пуле памяти, автоматически размещаются перед сбросом устройства.
Managed — Данные буфера вершин автоматически копируются в доступную устройству память по мере необходимости. Копия данных также сохраняется в буфере в системной памяти и всегда может быть заблокирована.
SystemMemory — Данныебуфера вершин размещаются в системной памяти, недоступной для устройства.
Scratch — Пул системной паяти не привязанный к устройству и, следовательно, недоступный для использования им. Этот вариант полезен для манипуляций с данными без их предварительной привязки к формату конкретного устройства.
В приложении с цветным треугольником из главы 1 относительно легко переместить данные треугольника в буфер вершин. Сперва объявим переменную буфера вершин, сразу после устройства:
private Device device = null; private VertexBuffer vb = null;
Вместо того, чтобы создавать данные каждый раз при визуализации сцены, мы значительно увеличим быстродействие, сделав это только один раз. Можно выполнить это сразу после создания устройства, так что переместите код создания треугольника следующим образом:
// Создание устройства device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams); CustomVertex.PositionColored[] verts = new CustomVertex.PositionColored[3]; verts[0].Position = new Vector3(0.0f, 1.0f, 1.0f); verts[0].Color = System.Drawing.Color.Aqua.ToArgb(); verts[1].Position = new Vector3(-1.0f, -1.0f, 1.0f); verts[1].Color = System.Drawing.Color.Black.ToArgb(); verts[2].Position = new Vector3(1.0f, -1.0f, 1.0f); verts[2].Color = System.Drawing.Color.Purple.ToArgb(); vb = new VertexBuffer(typeof(CustomVertex.PositionColored), 3, device, Usage.Dynamic | Usage.WriteOnly, CustomVertex.PositionColored.Format, Pool.Default); vb.SetData(verts, 0, LockFlags.None);
Единственное изменение — две новых строки после кода создания треугольника. Сначала мы создаем буфер вершин для хранения трех структур данных вершин, которые мы только что объявили. Мы хотим, чтобы буфер был доступен только для чтения, динамическим и находился в выбираемом по умолчанию пуле памяти для наилучшей производительности. Также нам надо поместить наш список треугольников в буфер вершин, и мы это делаем с помощью метода SetData. В качестве первого параметра этот метод принимает любой объект общего назначения (точно так же, как DrawUserPrimitives). Второй параметр — это смещение, начиная с которого мы располагаем данные, а поскольку сейчас мы хотим заполнить все данные, укажем здесь ноль. Последний параметр указывает, как будет заблокирован буфер на время записи наших данных. Вскоре мы обсудим различные механизмы блокировки; сейчас о том, как блокируется буфер беспокоиться не надо.
Скомпилировав приложение сейчас мы, конечно же, получим ошибку компиляции, поскольку вызову DrawUserPrimitives в OnPaint требуется переменная verts. Нам необходим способ сообщить Direct3D, что мы хотим визуализировать данные из нашего буфера вершин, а не из массива, который мы объявляли ранее.
Естественно, такой метод существует. Чтобы Direct3D при рисовании примитивов читал данные из нашего буфера вершин, необходимо вызвать метод устройства SetStreamSource. Вот как выглядят два перегруженных прототипа этой функции:
public void SetStreamSource(System.Int32 streamNumber, Microsoft.DirectX.Direct3D.VertexBuffer streamData, System.Int32 offsetInBytes, System.Int32 stride) public void SetStreamSource(System.Int32 streamNumber, Microsoft.DirectX.Direct3D.VertexBuffer streamData, System.Int32 offsetInBytes)
Единственное различие между этими двумя перегруженными версиями в том, что одна содержит параметр, задающий размер шага потока. Первый параметр — это номер потока, который мы используем для получения данных. Пока мы всегда будем указывать в этом параметре нуль, но в последующих главах обсудим использование нескольких потоков. Второй параметр — это буфер вершин, который хранит данные, используемые нами в качестве источника. Третий параметр представляет смещение (в байтах) в буфере вершин, начиная с которого Direct3D будет выбирать данные для рисования. Размер шага (имеющийся только в одной перегрузке) — это размер каждой вершины в буфере. Если при создании буфера вы указали тип хранящихся в нем вершин, использовать перегруженную версию с этим параметром не требуется.
Замените вызов функции рисования на следующий код:
device.SetStreamSource(0, vb, 0); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);
Как уже говорилось, мы соединяем нулевой поток с нашим буфером вершин и, поскольку хотим использовать все данные, устанавливаем смещение равным нулю. Обратите внимание, что мы поменяли и функцию рисования. Поскольку теперь все данные находятся в буфере вершин, нам больше не надо вызывать метод DrawUserPrimitives, поскольку эта функция предназначена для рисования определенных пользователем данных, передаваемых ей в параметре. Более общая функция DrawPrimitives рисует примитивы из потокового источника. У метода DrawPrimitives три параметра: первый — это тип примитивов, который мы уже обсуждали. Второй параметр — это начальная вершина в потоке. Последний параметр — это количество рисуемых примитивов.
Даже эта простая демонстрация рисования треугольника из буфера вершин показывает примено 10% рост быстродействия, отражающийся в частоте кадров. В последующих главах мы еще поговорим об измерении быстродействия и частоты кадров. Однако, у приложения остается одна проблема, становящаяся очевидной при попытке изменить размеры окна. При изменении размеров окна треугольник просто исчезает.
Есть несколько причин, вызывающих такое поведение, и две из них мы уже упомянули раньше. Вспомните предыдущую главу, где мы узнали, что при изменении размеров окна автоматически выполняется сброс устройства. Однако, когда ресурс создан в используемом по умолчанию пуле памяти (как наш буфер вершин), при сбросе устройства он создается заново. Итак, при изменении размеров окна выполняется сброс устройства и буфер вершин создается заново. Автоматическое воссоздание буфера вершин после сброса устройства — это одна из отличных возможностей DirectX для управляемого кода. Однако, в новом буфере нет данных, так что при следующей попытке рисования ничего отображено не будет.
У буфера вершин есть событие Create, которое сообщает нам, что буфер создан и готов принять данные. Нам необходимо изменить приложение, чтобы оно перехватывало это событие, и использовать его для заполнения буфера данными. Добавьте к приложению следующую функцию:
private void OnVertexBufferCreate(object sender, EventArgs e) { VertexBuffer buffer = (VertexBuffer)sender; CustomVertex.PositionColored[] verts = new CustomVertex.PositionColored[3]; verts[0].Position = new Vector3(0.0f, 1.0f, 1.0f); verts[0].Color = System.Drawing.Color.Aqua.ToArgb(); verts[1].Position = new Vector3(-1.0f, -1.0f, 1.0f); verts[1].Color = System.Drawing.Color.Black.ToArgb(); verts[2].Position = new Vector3(1.0f, -1.0f, 1.0f); verts[2].Color = System.Drawing.Color.Purple.ToArgb(); buffer.SetData(verts, 0, LockFlags.None); }
Функция соответствует стандартной сигнатуре обработчика сообщений и получает объект, сгенерировавший событие и список аргументов. Для рассматриваемого события список аргументов отсутствует, так что второй параметр можно игнорировать. Сперва мы получаем сгенерировавший событие буфер вершин для чего приводим тип члена sender к буферу вершин. Затем мы следуем тем же путем, что и ранее, создавая наши данные и вызывая SetData. Теперь мы должны изменить код создания треугольника в нашей функции InitializeGraphics, поместив вместо него следующие две строчки:
vb.Created += new EventHandler(this.OnVertexBufferCreate); OnVertexBufferCreate(vb, null);
Код подключает обработчик события создания к буферу вершин, что гарантирует вызов нашего метода OnVertexBufferCreate при каждом создании буфера вершин. Поскольку событие не будет перехвачено при первом создании объекта, нам необходимо вручную вызвать нашу функцию обработки событий. Запустите приложение еще раз и попробуйте изменить размеры окна.
Вы успешно обновили простое приложение рисующее треугольник с неэффективной версии до более эффективной, использующей видеопамять и буфер вершин. Однако, единственный вращающийся треугольник по прежнему выглядит скучно. Давайте попытаемся сделать что-нибудь более впечатляющее. Обычно, первая вещь, которую пытаются визуализировать — это куб, и пришло время заняться этим. Код будет более сложным, но все основные принципы по-прежнему применимы.
Вся геометрия трехмерной сцены состоит из треугольников, так как же визуализировать куб? Мы можем составить квадрат из двух треугольников, а куб состоит из шести квадратов. Нам нужны только координаты восьми вершин куба, и из них мы сможем создать куб. Давайте изменим код создания геометрии так, как показано в листинге 3.1:
CustomVertex.PositionColored[] verts = new CustomVertex.PositionColored[36]; // Передняя грань verts[0] = new CustomVertex.PositionColored(-1.0f, 1.0f, 1.0f, Color.Red.ToArgb()); verts[1] = new CustomVertex.PositionColored(-1.0f, -1.0f, 1.0f, Color.Red.ToArgb()); verts[2] = new CustomVertex.PositionColored( 1.0f, 1.0f, 1.0f, Color.Red.ToArgb()); verts[3] = new CustomVertex.PositionColored(-1.0f, -1.0f, 1.0f, Color.Red.ToArgb()); verts[4] = new CustomVertex.PositionColored( 1.0f, -1.0f, 1.0f, Color.Red.ToArgb()); verts[5] = new CustomVertex.PositionColored( 1.0f, 1.0f, 1.0f, Color.Red.ToArgb()); // Задняя грань (помните, что эта грань направлена от камеры // и вершины должны быть упорядочены по часовой стрелке) verts[6] = new CustomVertex.PositionColored(-1.0f, 1.0f, -1.0f, Color.Blue.ToArgb()); verts[7] = new CustomVertex.PositionColored( 1.0f, 1.0f, -1.0f, Color.Blue.ToArgb()); verts[8] = new CustomVertex.PositionColored(-1.0f, -1.0f, -1.0f, Color.Blue.ToArgb()); verts[9] = new CustomVertex.PositionColored(-1.0f, -1.0f, -1.0f, Color.Blue.ToArgb()); verts[10] = new CustomVertex.PositionColored( 1.0f, 1.0f, -1.0f, Color.Blue.ToArgb()); verts[11] = new CustomVertex.PositionColored( 1.0f, -1.0f, -1.0f, Color.Blue.ToArgb()); // Верхняя грань verts[12] = new CustomVertex.PositionColored(-1.0f, 1.0f, 1.0f, Color.Yellow.ToArgb()); verts[13] = new CustomVertex.PositionColored( 1.0f, 1.0f, -1.0f, Color.Yellow.ToArgb()); verts[14] = new CustomVertex.PositionColored(-1.0f, 1.0f, -1.0f, Color.Yellow.ToArgb()); verts[15] = new CustomVertex.PositionColored(-1.0f, 1.0f, 1.0f, Color.Yellow.ToArgb()); verts[16] = new CustomVertex.PositionColored( 1.0f, 1.0f, 1.0f, Color.Yellow.ToArgb()); verts[17] = new CustomVertex.PositionColored( 1.0f, 1.0f, -1.0f, Color.Yellow.ToArgb()); // Нижняя грань (помните, что эта грань направлена от камеры // и вершины должны быть упорядочены по часовой стрелке) verts[18] = new CustomVertex.PositionColored(-1.0f, -1.0f, 1.0f, Color.Black.ToArgb()); verts[19] = new CustomVertex.PositionColored(-1.0f, -1.0f, -1.0f, Color.Black.ToArgb()); verts[20] = new CustomVertex.PositionColored( 1.0f, -1.0f, -1.0f, Color.Black.ToArgb()); verts[21] = new CustomVertex.PositionColored(-1.0f, -1.0f, 1.0f, Color.Black.ToArgb()); verts[22] = new CustomVertex.PositionColored( 1.0f, -1.0f, -1.0f, Color.Black.ToArgb()); verts[23] = new CustomVertex.PositionColored( 1.0f, -1.0f, 1.0f, Color.Black.ToArgb()); // Левая грань verts[24] = new CustomVertex.PositionColored(-1.0f, 1.0f, 1.0f, Color.Gray.ToArgb()); verts[25] = new CustomVertex.PositionColored(-1.0f, -1.0f, -1.0f, Color.Gray.ToArgb()); verts[26] = new CustomVertex.PositionColored(-1.0f, -1.0f, 1.0f, Color.Gray.ToArgb()); verts[27] = new CustomVertex.PositionColored(-1.0f, 1.0f, -1.0f, Color.Gray.ToArgb()); verts[28] = new CustomVertex.PositionColored(-1.0f, -1.0f, -1.0f, Color.Gray.ToArgb()); verts[29] = new CustomVertex.PositionColored(-1.0f, 1.0f, 1.0f, Color.Gray.ToArgb()); // Правая грань (помните, что эта грань направлена от камеры // и вершины должны быть упорядочены по часовой стрелке) verts[30] = new CustomVertex.PositionColored( 1.0f, 1.0f, 1.0f, Color.Green.ToArgb()); verts[31] = new CustomVertex.PositionColored( 1.0f, -1.0f, 1.0f, Color.Green.ToArgb()); verts[32] = new CustomVertex.PositionColored( 1.0f, -1.0f, -1.0f, Color.Green.ToArgb()); verts[33] = new CustomVertex.PositionColored( 1.0f, 1.0f, -1.0f, Color.Green.ToArgb()); verts[34] = new CustomVertex.PositionColored( 1.0f, 1.0f, 1.0f, Color.Green.ToArgb()); verts[35] = new CustomVertex.PositionColored( 1.0f, -1.0f, -1.0f, Color.Green.ToArgb()); buffer.SetData(verts, 0, LockFlags.None);
Да, пришлось напечатать много вершин, 36 если быть точным, но не волнуйтесь. Можно загрузить исходный код с прилагаемого к книге компакт-диска. Как уже упоминалось, куб состоит из 12 треугольников, а у каждого треугольника три вершины, что и дает нам приведенный список вершин. Запустив код сейчас мы получим исключение при вызове SetData. Можете предположить почему? Если вы предположили, что мы не изменили исходный размер нашего буфера вершин, то вы правы. Необходимо сделать еще пару изменений, прежде чем мы сможем запустить приложение. Обновите показанные ниже строки:
vb = new VertexBuffer(typeof(CustomVertex.PositionColored), 36, device, Usage.Dynamic | Usage.WriteOnly, CustomVertex.PositionColored.Format, Pool.Default); device.Transform.World = Matrix.RotationYawPitchRoll(angle / (float)Math.PI, angle / (float)Math.PI * 2.0f, angle / (float)Math.PI); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 12);
Главная особенность — изменение размера буфера вершин, создаваемого для хранения данных, которые мы хотим визуализировать. Также мы изменяем вращение кубов, чтобы сделать его немного более сумашедшим. И, наконец, мы меняем вызов визуализации для того, чтобы рисовать 12 примитивов, а не единственный треугольник, как мы делали раньше. Поскольку куб является полностью сформированным сплошным трехмерным объектом, нам больше не надо видеть обратные стороны треугольников. Мы можем использовать предлагаемый Direct3D по умолчанию режим отбрасывания невидимых граней (против часовой стрелки). Вернитесь к коду и удалите из него строку установки режима отбрасывания невидимых граней. Теперь попробуйте запустить приложение.
Потрясающе, мы получили раскрашенный куб, вращающийся на экране. Каждая грань куба окрашена в свой цвет и вращение куба позволяет нам видеть все его грани не отключая отбрасывание невидимых граней. Если вы хотите в сцене визуализировать несколько кубов, надеюсь, вы не будете создавать набор буферов вершин, по одному для каждого куба. Есть гораздо более простой способ сделать это.
Теперь мы нарисуем три расположенных рядом куба. Поскольку сейчас камера расположена так, что единственный куб занимает почти всю сцену, мы сперва отодвинем камеру немного назад. Измените функцию LookAt следующим образом:
device.Transform.View = Matrix.LookAtLH(new Vector3(0, 0, 18.0f), new Vector3(), new Vector3(0, 1, 0));
Как видите, мы просто переместили местоположение нашей камеры назад, чтобы видеть большую часть сцены. Запустив приложение сейчас вы увидите тот же самый вращающийся куб, только теперь он выглядит меньше, поскольку камера находится дальше от него. Чтобы нарисовать еще два куба по бокам от первого мы можем повторно использовать существующий буфер вершин, и просто сообщить Direct3D, чтобы он нарисовал те же самые вершины снова. Добавьте следующие строки кода после вызова DrawPrimitives:
device.Transform.World = Matrix.RotationYawPitchRoll(angle / (float)Math.PI, angle / (float)Math.PI / 2.0f, angle / (float)Math.PI * 4.0f) * Matrix.Translation(5.0f, 0.0f, 0.0f); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 12); device.Transform.World = Matrix.RotationYawPitchRoll(angle / (float)Math.PI, angle / (float)Math.PI * 4.0f, angle / (float)Math.PI / 2.0f) * Matrix.Translation(-5.0f, 0.0f, 0.0f); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 12);
Что же мы делаем здесь? Direct3D уже знает вершины какого типа мы собираемся рисовать, поскольку свойство VertexFormat мы установили при рисовании первого куба. Он также знает из какого буфера вершин получать данные, благодаря функции SetStreamSource, также использовавшейся для первого куба. Так что же еще надо знать Direct3D чтобы нарисовать второй (и третий) куб? Осталось только сообщить где мы хотим их нарисовать.
Установка мирового преобразования «перемещает» наши данные из пространства модели в мировое пространство, так что мы используем в качестве нашей матрицы преобразования? Во-первых мы выполняем вращение, также как делали это в функции SetupCamera; здесь мы используем различные функции вращения просто для того, чтобы кубы вращались по разному. Вторая часть мирового преобразования является новой. Мы умножаем Matrix.Translation на существующую матрицу вращения. Матрица перемещения позволяет передвигать вершины в мировом пространстве с одного места на другое. Взгляните на параметры перемещения: мы сдвигаем второй куб на пять единиц вправо, а третий — на пять единиц влево.
Обратите внимание, что мы перемножаем две матрицы преобразований, в результате чего получаем общее преобразование, объединяющее два аргумента. Оно будет выполняться в том порядке, в котором указаны матрицы, так что в данном случае нащи вершины сперва будут вращаться, а затем перемещаться. Перемещение с последующим вращением приведет к совсем другому результату. При преобразовании вершин очень важно помнить о порядке операций.
На CD-ROM приведен слегка измененный код, который рисует девять кубов, а не три как здесь (рис. 3.1).
Рис. 3.1. Раскрашенные кубы
netlib.narod.ru | < Назад | Оглавление | Далее > |