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)

Опишем назначение параметров:

СОЗДАНИЕ БУФЕРА ВЕРШИН ЧЕРЕЗ НЕУПРАВЛЯЕМЫЙ COM-УКАЗАТЕЛЬ
Как и для устройства, для буфера вершин есть перегруженный конструктор с параметром IntPtr. Это значение используется для передачи фактического укзателя COM-интерфейса для неуправляемого интерфейса IDirect3DVertexBuffer9. Это полезно, когда вы хотите использовать буфер вершин полученный из внешнего (неуправляемого) источника. Полностью управляемые приложения никогда не используют этот конструктор, а значение этого параметра не может быть равно null.

В приложении с цветным треугольником из главы 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:

Листинг 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. Раскрашенные кубы

Рис. 3.1. Раскрашенные кубы



netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz