Приложение 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, но его реализация оставлена вам как домашнее задание.

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