Приложение MFC D3D – руководство по Direct3D часть III - Буферы вершин

ОГЛАВЛЕНИЕ

Буферы вершин

Буферы вершин являются ресурсами Direct3D, существующими в памяти, представленными их собственным интерфейсом IDirect3DVertexBuffer9, отрисовываемым с помощью методов устройства.

Формат буфера вершин гибок по структуре. Это позволяет приложениям расширить определение вершины от простых данных о положении (x,y,z) до данных о положении плюс нормаль, размера точки, размытия цвета, отражения цвета, до 8 наборов данных текстурных координат, до 3 весов смешивающихся вершин, или заранее преобразованных и освещенных вершин, иначе называемых TL-вершинами, для использования конвейером фиксированных вершин или конвейером программируемых вершин.

Начнем с простого гибкого формата вершин (FVF), содержащего непреобразованные данные о положении и размытый цвет.

// сначала определяется формат как комбинация флагов D3DFVF:
#define D3DFVF_CUSTOMVERTEX D3DFVF_XYZ | D3DFVF_DIFFUSE

// далее определяется пользовательская вершина как:
struct CUSTOMVERTEX
{
    FLOAT x, y, z;  // положение
 DWORD color;  // размытый цвет ARGB
};

// так что можно создать квадрат со следующими данными:
static CUSTOMVERTEX s_Vertices[] =
{
 // x      y     z     цвет ARGB
 { -1.0f, -1.0f, 0.0f, 0xFFFF0000 }, // нижний левый угол, красный
 { +1.0f, -1.0f, 0.0f, 0xFF00FF00 }, // нижний правый угол, зеленый
 { +1.0f, +1.0f, 0.0f, 0xFF0000FF }, // верхний правый угол, синий
 { -1.0f, +1.0f, 0.0f, 0xFFFFFFFF }, // верхний левый угол, белый
};

Если вы хотите добавить пару текстурных наборов координат (u,v), и FVF, и структура пользовательской вершины должны соответствовать им с использованием дополнительного макроса D3DFVF:

#define D3DFVF_CUSTOMVERTEX D3DFVF_XYZ | \
        D3D_FVF_DIFFUSE         | \
        D3DFVF_TEX2             | \ // 2 набора текстурных координат
        D3DFVF_TEXCOORDSIZE2(0) | \ // 1й набор (в индексе 0) имеет размер 2
        D3DFVF_TEXCOORDSIZE2(1)     // 2й набор (в индексе 1) имеет размер 2

struct CUSTOMVERTEX
{
    FLOAT x, y, z;   // положение
    DWORD color;     // размытый цвет ARGB
    FLOAT tu1, tv1;  // 1й набор текстурных координат u,v
    FLOAT tu2, tv2;  // 2й набор текстурных координат u,v
};

static CUSTOMVERTEX s_Vertices[] =
{
 // x      y     z     цвет ARGB  u1    v1    u2    v2
 { -1.0f, -1.0f, 0.0f, 0xFF0000FF, 1.0f, 1.0f, 1.0f, 1.0f },
 { +1.0f, -1.0f, 0.0f, 0xFFFF0000, 0.0f, 1.0f, 0.0f, 1.0f },
 { +1.0f, +1.0f, 0.0f, 0xFFFFFF00, 0.0f, 0.0f, 0.0f, 0.0f },
 { -1.0f, +1.0f, 0.0f, 0xFFFFFFFF, 1.0f, 0.0f, 1.0f, 0.0f },
};

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

// создать буфер вершин
LPDIRECT3DVERTEXBUFFER9 pVB;

m_pd3dDevice->CreateVertexBuffer(4 * sizeof(CUSTOMVERTEX),
            D3DUSAGE_WRITEONLY,
            D3DFVF_CUSTOMVERTEX,
            D3DPOOL_DEFAULT,
            &pVB,
            NULL);

// заблокировать его вершины и перенести данные в них
void* pVerts;

pVB->Lock(0, 0, &pVerts, 0);
memcpy(pVerts, s_Vertices, 4 * sizeof(CUSTOMVERTEX));

// перенос завершен, производится его разблокировка
pVB->Unlock();

...

// отрисовать буфер вершин
m_pd3dDevice->SetStreamSource(0, pVB, sizeof(CUSTOMVERTEX));
m_pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX);
m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLEFAN, 0, 2);

Здесь лежит мощь FVF: если (a) использовать правильные комбинации флагов для его определения, (b) объявить элементы вершин с правильными типами и в правильном порядке, и (c) использовать правильное количество вершин и размер компонента вершины в методах устройства, то устройство будет знать, как отрисовать их.

Тема форматов вершин SDK легко усваивается, поэтому опробуйте ее; игнорируйте все, относящееся к смешению весов (однозначно сложной теме) и, возможно, текстурные координаты, которые будут рассмотрены в итоге.

Метод устройства CreateVertexBuffer принимает размер в байтах, всегда количество вершин умножается на размер компонента вершины; метод также принимает определение FVF. pVB – фактический возвращаемый буфер, и он должен быть в пуле по умолчанию для максимальной производительности, что достигается в сочетании с использованием только для записи; отсюда следует, что нельзя будет читать данные из буфера вершин, но это обычно не нужно, как минимум не в данном примере. Кстати, это сочетание пула и использования столь важно в плане производительности, что справка Direct3D SDK утверждает, что "Буферы, созданные с D3DPOOL_DEFAULT, не задающие D3DUAGE_WRITEONLY, могут испытать сильное снижение производительности", так что лучше один раз не противоречить Microsoft и всегда использовать ее для создания буферов вершин.

Термины «компоненты вершин» и «элементы вершин» указывались несколько неточно; надо пояснить.

Буфер вершин – поток данных. Поток – однородный массив компонентов данных. Каждый компонент состоит из отдельных элементов данных. Шаг потока – размер одного компонента в байтах.

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

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

// поток 0, положение, размытый, отражающий
#define D3DFVF_POSCOLORVERTEX D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_SPECULAR

struct POSCOLORVERTEX
{
    FLOAT x, y, z;
    DWORD diffColor, specColor;
};

// поток 1, текстурная координата 0
#define D3DFVF_TEXCOORDS0 D3DFVF_TEX0

struct TEXCOORDS0VERTEX
{
    FLOAT tu1, tv1;
};

// поток 2, текстурная координата 1
#define D3DFVF_TEXCOORDS1 D3DFVF_TEX1

struct TEXCOORDS1VERTEX
{
    FLOAT tu2, tv2;
};
...
// при условии, что созданы и инициализированы буферы для каждого FVF...
m_pd3dDevice->SetStreamSource(0, m_pVBPosColor,   0, sizeof(POSCOLORVERTEX));
m_pd3dDevice->SetStreamSource(1, m_pVBTexCoords0, 0, sizeof(TEXCOORDS0VERTEX));
m_pd3dDevice->SetStreamSource(2, m_pVBTexCoords1, 0, sizeof(TEXCOORDS1VERTEX));

// одного вызова DrawPrimitive достаточно для 3 потоков из 42 треугольников
m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLEFAN, 0, 42);

Член MaxStreams структуры функциональных возможностей устройства указывает максимальное количество одновременных потоков и должен быть в интервале от 1 до 16.

Была пропущена инициализация буферов вершин, использующая механизм блокировки и разблокировки, поэтому покончим с ней.

Блокировка ресурса означает предоставление центральному процессору доступа к его хранилищу, следовательно, приложение должно отказаться от такого доступа путем разблокировки ресурса после завершения его использования. Интерфейс каждого ресурса имеет свой собственный метод блокировки и разблокировки; буферы вершин (и индексов) принимают смещение в данных вершин для блокировки в байтах и размер данных вершин для блокировки в байтах; если оба аргумента установлены в 0 (как в показанном примере), то блокируется весь буфер и возвращается в буфер памяти, заданный как третий аргумент.

Последний аргумент описывает тип производимой блокировки со значением 0 или сочетания флагов D3DLOCK. Флаги только намекают на запланированное использование заблокированных данных; например, D3DLOCK_NOOVERWRITE говорит, что приложение обещает не перезаписывать возвращенный буфер, что позволяет драйверу продолжать отрисовку буфера вершин даже при блокировке. Однако вы можете действовать против этих флагов с неожиданными результатами, снижением производительности и, скорее всего, без поддержки в будущих выпусках Direct3D, поэтому лучше оставить последний аргумент со значением 0, если вы не знаете точно, что делаете.

Кстати, смещение для закрепления и размер для закрепления позволяет закрепить порции буфера вершин, но помните, что это не влечет за собой блокировку геометрической части модели; если вы аккуратно спроектируете модель вертолета, то, возможно, сможете заблокировать первые 42 компонента вершин для анимирования главных винтов, а впоследствии заблокировать вершины с 358 по 400 для анимирования хвостового винта, но это вовсе не лучший способ сделать это. Лучше держать отдельные буферы для каждой части модели.

Метод IDirect3DDevice9::DrawPrimitive отрисовывает последовательность неиндексированных геометрических примитивов заданного типа из текущего набора входных потоков данных. Он требует предыдущий IDirect3DDevice9::SetFVF, если используется пользовательский FVF (отличный от D3DFVF_XYZ). Никогда нельзя вызывать его для отрисовки одного треугольника, и член MaxPrimitiveCount функциональных возможностей устройства показывает, сколько примитивов он может обработать за один вызов, хотя на практике слишком много примитивов дают, скажем, 0.55 кадров в секунду. Примитивы являются списками точек, линий или треугольников, последние сгруппированы в список, ленту или веер. Выбор данного типа зависит исключительно от вашего приложения, но помните, что каждый тип по-разному представляет данные.

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

Буферы индексов

Буферы индексов, представленные интерфейсом IDirect3DIndexBuffer9, являются буферами памяти, содержащими данные индексов. Данные индексов или индексы являются целочисленными смещениями в буферах вершин и используются для отрисовки примитивов с помощью метода IDirect3DDevice9::DrawIndexedPrimitive.

Как ресурсы, по сути, буферы индексов имеют формат, использование и пул. Формат ограничен D3DFMT_INDEX16 или D3DFMT_INDEX32, указывающими битовую глубину буфера. Предпочтительный пул - D3DPOOL_DEFAULT, исключая некоторые конфигурации, использующие память AGP. Использование лучше всего оставить с D3DUSAGE_WRITEONLY, если не надо читать из буферов индексов (при этом лучшим пулом будет системная память). Использование также может навязать программную обработку индексов (индексы, как вершины, могут обрабатываться аппаратно) при использовании смешанного VP, но должна быть веская причина поступить так.

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

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

1---3
|\  |
| \ |
|  \|
0---2

Альтернатива – создать буфер вершин всего с четырьмя отдельными вершинами и установить буфер индексов так, чтобы первый треугольник рисовался с вершинами (0,1,2), а второй – с вершинами (1,3,2). Содержимое буфера индексов - 0,1,2,1,3,2.

Вы можете считать избыточностью то, что сейчас требуется 10 элементов данных для рисования исходных 4 (!), и вы правы, за исключением следующей двусоставной выгоды: во-первых, буфер индексов хранит целые значения, занимающие меньше памяти и обрабатываемые во много раз быстрее, чем значения с плавающей точкой; во-вторых, индексированные вершины хранятся адаптером в кеше вершин, из которого вершины 1 и 2 можно достать, чтобы нарисовать второй треугольник, так как они недавно использовались для рисования первого треугольника. Это избавляет от повторного чтения из буфера вершин и значительно повышает производительность.

Можно еще повысить эффективность буфера индексов с помощью типа примитива; показанное содержимое используется для списка треугольников, предполагающего, что треугольники не связаны. Если взамен использовать веер треугольников, в котором одна вершина общая у всех треугольников, буфер сокращается до 1,3,2,0, потому что система понимает, что вы хотите использовать (3,2,1) для первого треугольника и (2,0,1) –  для второго. Аналогично, если используется лента треугольников, исходный буфер индексов сокращается до 0,1,2,3, потому что лента полагает, что треугольники соединены, и рисует (0,1,2) и (1,3,2). В обоих случаях экономятся два индекса.

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

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