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

ОГЛАВЛЕНИЕ

Третья часть руководства по Direct3D

Введение

Вот, наконец, долгожданная часть III специализированного каркаса Direct3D и руководства по 3D, инкапсулированного в класс CXD3D. В части I были рассмотрены некоторые концепции 3D, архитектура Direct3D и объект перечисления; там также есть демонстрационный проект. В части II был рассмотрен выбор параметров 3D из объектов перечисления и цикл отрисовки. В этой части будут рассмотрены координатные пространства и преобразования из одного в другое, наряду с заполнителями фактических геометрических данных или буферами.

Преобразования

Преобразования используются для перевода геометрии объекта из одного координатного пространства в другое. Самые распространенные преобразования производятся с помощью матриц. Матрица – необходимый инструмент для хранения значений преобразования и применения их к данным.

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

Преобразование мира (или матрица мира) помещает все объекты в сцене в одну базовую систему координат. Представьте единичный куб с центром в начале координат; если матрица мира хранит сдвиг (5,5,5), куб сдвигается на 5 единиц вправо, на 5 единиц вверх и на 5 единиц назад. Формально объект был переведен из пространства объектов в пространство мира.

В пространстве объектов (иначе называемом  «пространство модели» или «локальное пространство») все вершины объекта определены относительно общего начала координат. Например, определен единичный куб с центром в (0,0,0), так чтобы его верхний передний правый угол был в (0.5,0.5,-0.5); в пространстве мира этот угол фактически находится в (5.5,5.5,4.5).

Преобразование мира может содержать любое сочетание сдвигов, поворотов и масштабирований, если соблюдаются правила алгебры матриц для ее построения. Все же большинство приложений должно избегать слишком частого изменения преобразования мира из-за производительности.

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

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

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

После помещения объектов в мир в него помещается зритель; следовательно, мировые координаты объектов преобразуются с помощью преобразования вида.

Преобразование вида (или матрица вида) преобразует вершины в пространстве мира в пространство зрителя, глаза или камеры. Начнем со зрителя в начале координат, смотрящего в положительный z, в пространстве мира: матрица вида является единичной матрицей, и зритель видит единичный куб, висящий там  (в 5,5,5). Теперь зритель поднимается на 10 единиц; куб опускается до (5,-5,5) в пространстве камеры и, вероятно, уже не виден. Теперь зритель поворачивается до тех пор, пока не смотрит прямо вниз; куб поворачивается вдоль оси x на 90 градусов против часовой стрелки, пока не останавливается  (в 5,5,5) в пространстве камеры; если зритель сейчас сдвинется вперед (в положительный z пространства мира), куб опустится в пространстве камеры. Это похоже на игру в кошки-мышки с объектами, двигающимися и поворачивающимися на ровно противоположную величину того, что делает зритель.

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

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

Функция расширений Direct3D D3DXMatrixLookAtLH облегчает установку матрицы вида, если дано расположение глаза зрителя и направление взгляда в пространстве мира. Она также принимает направление вверх, обычно (0,1,0). Кстати, если 'вверх' - (0,-1,0), то используется свойство инверсии оси y, востребованное в некоторых играх-боевиках от первого лица и в симуляторах полета, в которых если толкнуть рычаг или рукоятку вверх или вперед, то пикируешь.

Также есть функция D3DXMatrixLookAtRH для правых систем координат, но Direct3D использует левую систему координат, в которой большой палец левой руки указывает в направлении положительного z. Использование (или перенос из) правой системы координат непросто в Direct3D. Это влияет на компоновку данных и на преобразования, поэтому обязательно прочитайте справку SDK(средства разработки программного обеспечения), если вам не остаётся ничего, кроме как использовать правую систему координат, но это не входит в данную статью.

Объекты подвергаются еще одному преобразованию –  преобразованию проекции. Она самая сложная; для начала представьте ее как внутренности камеры, сродни выбору объектива для камеры.

Камеры имеют определенное поле зрения (FOV), попросту говоря, максимальное пространство, которое они могут охватить. Люди, не попадающие на семейную фотографию, просто не входят в горизонтальное FOV камеры, а верхушка дерева за людьми не входит в вертикальное FOV. Если вы приблизите камеру, меньше людей попадет на фотографию, а если вы отдалите камеру, больше людей попадет на фотографию, следовательно, на FOV влияет степень увеличения или изменение масштаба.

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

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

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

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

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

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

Формально преобразование проекции преобразует все внутри пирамиды вида (в пространстве камеры) в пространство проекции. На практике матрица проекции выполняет сдвиг и масштабирование на основе параметров пирамиды вида. Математические подробности такой конструкции детально не рассмотрены, подробней читайте о ней в документации SDK; набор функций D3DXMatrixPerspective* обертывает построение матрицы проекции, дойдем до него непосредственно после рассмотрения последнего геометрического преобразования.

Спроецированная трехмерная сцена в итоге представляется на двухмерном (плоском) прямоугольном участке внутри цели отрисовки, иначе называемой прямоугольником окна просмотра. Это последнее преобразование системы координат из пространства проекции в плоскость экрана, или, если хотите, из пространства проекции в прямоугольник окна просмотра.

Адресация пикселей назначает верхний левый угол экрана началом координат; следовательно, направление положительный y в действительности идет вниз, прямо противоположно ему же в пространстве проекции. Более того, прямоугольник окна просмотра может пересчитаться до всей площади цели отрисовки, но не обязательно, в случае чего требуется дополнительное масштабирование до размеров окна просмотра. Это преобразование выполняется внутренне Direct3D, но его можно настроить путем изменения параметров окна просмотра и метода устройства SetViewport.

Члены X, Y, Width и Height структуры D3DVIEWPORT9 описывают положение и размеры (в пикселях) прямоугольника окна просмотра на поверхности цели отрисовки. Если приложения отрисовывают на всю целевую поверхность, скажем, поверхность 640x480, то эти члены должны быть установлены в 0, 0, 640 и 480, соответственно. Структура также хранит диапазон глубины в членах MinZ и MaxZ, но не путайте их с плоскостями отсечения; эти члены обычно установлены в 0.0 и 1.0, чтобы показать, что надо отрисовать весь диапазон значений глубины пространства проекции, но могут быть установлены в другие значения получения особых эффектов. Например, можно установить оба члена в 0.0, чтобы заставить систему отрисовать объекты на переднем плане сцены, как микродисплей для чтения на ходу, или оба в 1.0, чтобы загнать другие объекты на задний план, как небо.

Такова связь между установкой преобразования проекции и прямоугольником окна просмотра. Это удобный обходной прием для вычисления коэффициентов масштабирования x и y матрицы проекции.

Вместо предоставления горизонтального и вертикального полей зрения, исполняющих сложные тригонометрические функции для определения параметров масштабирования, можно использовать ширину и высоту окна просмотра для разложения на множители ближней плоскости отсечения и получения таких же результатов. Это вытекает из тригонометрии пирамиды вида, и Direct3D использует это, чтобы сэкономить хотя бы одну затратную операцию 1/tan(0.5 * FOV). Кстати, нет функции D3DXMatrixPerspective*, принимающей оба поля зрения; они не принимают ни одно или принимают только вертикальное поле зрения вместе с отношением сторон окна просмотра, отношением ширины к высоте.

В основанном на каркасе типичном приложении размеры окна просмотра внутренне устанавливаются соответствующими параметрам представления BackBufferWidth и BackBufferHeight, так что, наконец, матрицу проекции можно легко установить, как в следующем примере:

// установить матрицу проекции
float fAspect = (float)m_d3dpp.BackBufferWidth /
                (float)m_d3dpp.BackBufferHeight;

D3DXMatrixPerspectiveFovLH(&m_matProj,  // вывод
                           D3DX_PI / 4, // вертикальное FOV
                           fAspect,     // отношение сторон окна просмотра
                           1.0e-1f,     // ближняя плоскость отсечения z
                           1.0e+3f);    // дальняя плоскость отсечения z

m_pd3dDevice->SetTransform(D3DTS_PROJECTION, &m_matProj);

Ниже обоснованы типичные значения аргументов.

Человеческий глаз владеет 180° (PI) полем зрения, но человек обычно обращает внимание на 45° посередине, поэтому 45° (PI/4) – типичное значение для поля зрения. Кстати, хороший водитель машины невольно приучает глаза к полю зрения 120° и более, чтобы охватить дорогу и каждое зеркало заднего вида, и быстро читающие люди используют тот же прием. При этом есть птицы, владеющие 360°, но они не играют в трехмерные игры. Чтобы добиться некоторого правдоподобия в сценах, используйте нечто между PI/3 и PI. Чтобы добиться эффекта искажения объектива "рыбий глаз" (широкоугольного), используйте нечто между 100° и чуть меньше 180°, так как Direct3D признает PI или нечто больше его за правильное поле зрения.

Отношение сторон типичных полноэкранных дисплеев - 4:3 или 1.33:1, вытекая из 1024/768, 800/600 и даже 640/480, или ширина в 1.33 раза больше высоты, кстати, то же самое используется для стандартного телевидения. Телевидение высокой чёткости имеет отношение сторон 16:9, а типичные отношения фильмов - 1.85:1 и 2.35:1, то есть ширина вдвое больше высоты. Всегда лучше соответствовать размерам окна просмотра для достижения правдоподобия, но можно настроить отношение сторон для растягивания или сжатия изображения, чтобы получить некоторый особый эффект.

Плоскости отсечения, ах, плоскости отсечения! Значения столь же важны, как и отношение между ними. В показанном выше примере объекты, столь близкие, как 0.1 единиц, и столь дальние, как 1000 единиц, будут отрисованы, но отношение между плоскостями равно 10000, что максимально для типичных приложений. Дело в том, что отношение сильно влияет (даже в идеале) на распределение значений глубина по диапазону буфера глубины, особенно для чаще всего поддерживаемых 16-битных z-буферов. Это может вызвать искажения отрисовки между близкими объектами (именуемые войной за буфер глубины) и скрытые искажения поверхности в дальних объектах. Поэтому рекомендуется держать переднюю плоскость как можно дальше и соблюдать отношение 1000 к 10000, как советует справка SDK.

Совет

Есть  много изображений, помогающих понять эти концепции, в интернете и в справке SDK, но они умышленно не были включены. Они являются проекциями на бумаге (или на веб-страницах), и такова суть 3D в компьютере, не так ли? Попытайтесь мысленно представить эти словесные образы, так как это улучшит ваши навыки трехмерной визуализации. Используйте ваш ум, читая снова, пока не поймете.

Заключительные слова о трехмерных преобразованиях

Что еще можно сказать о них? Во-первых, их нельзя избежать; во-вторых, они являются частью состояния отрисовки, поэтому не меняйте их слишком часто; в-третьих, матрица проекции вообще не меняется в большинстве приложений, поэтому обычно она устанавливается в RestoreDeviceObjects и не трогается в течение всего остального цикла отрисовки; в-четвертых, в ряде случаев можно объединить матрицы мира и вида в единую матрицу мира-вида и использовать ее как матрицу мира, улучшая производительность; в-пятых, аккуратно выбирайте плоскости отсечения; и наконец, настраивайте матрицы, чтобы узнать, что произойдет!

Наконец, используйте класс CD3DArcBall (d3dutil.h) для установки матрицы мира (или вида) из ввода мыши; большинство примеров SDK делают это, позволяя вам вращать, сдвигать и масштабировать всю сцену с помощью мыши. Кстати, к классу был добавлен обработчик для сообщения WM_MOUSEWHEEL, но его реализация оставлена вам как домашнее задание.

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


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

Буферы вершин являются ресурсами 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). В обоих случаях экономятся два индекса.

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

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