Буфер глубины (depth buffer)
Дата создания: 2010-03-18 14:07:57
Последний раз редактировалось: 2012-03-01 02:48:07
Сегодня мы добавим в наши программы буфер глубины. Буфер глубины позволит устранить довольно неприятный дефект, который можно увидеть на следующей картинке:
В данном примере я использовал куб, две противоположные вершины которого окрашены в разные цвета: передний верхний угол - красный цвет, задний нижний угол - зелёный цвет.
На рисунке а объект нарисован неправильно. Здесь мы видим "внутренности " (задние треугольники) модели. Именно так рисовались все наши объекты до сих пор. На рисунке б показан правильно нарисованный объект. Собственно, наша задача в сегодняшнем уроке - научиться рисовать треугольники в правильном порядке.
Почему куб на рисунке а выводится неправильно? По умолчанию Direct3D выводит треугольники в том порядке, в каком они были определены. Обратите внимание на правую стенку куба на рисунке а. Здесь хорошо видно, в каком порядке выводились треугольники: передняя стенка, задняя, правая, нижняя.
Этот пример позволяет понять всю важность порядка вывода треугольников. Треугольники должны выводиться в порядке от самого дальнего к самому ближнему. Для определения того, какой треугольник расположен ближе к камере, так или иначе используются z координаты.
Сортировка треугольников
Прежде чем рассмотреть каким образом определяется порядок треугольников в DirectX, сначала мы узнаем о способе использовавшемся ранее.
Самый простой случай - когда треугольники расположены параллельно проекционной плоскости:
На рисунке а - вид сверху, на рисунке б - конечное изображение. В данном случае очень легко определить порядок треугольников:
1. Нужно взять z значение любых вершин обоих треугольников.
2. Отсортировать (например, с помощью метода пузырька) треугольники по z значению.
3. Вывести треугольники в порядке уменьшения z: сначала будут выведены дальние треугольники, затем ближние.
Если один из треугольников (или оба) не параллелен проекционной плоскости, то обычно берут среднее z-значение трёх вершин или z-значение только первой вершины. В любом случае возникнет некоторая погрешность (на самом деле она небольшая и ей можно пренебречь).
Самое главное здесь: треугольники сортируются по z-значению, а затем выводятся в зависимости от z-значения: от самого большого, к самому маленькому.
Существуют более сложные случаи расположения треугольников, где простая сортировка выдаст неверный результат:
Расположение треугольников, показанное на этой картинке маловероятно в реальных ситуациях, но тем не менее этот случай стоит рассмотреть. Здесь нельзя просто воспользоваться сортировкой по z координатам. Чтобы правильно отобразить эти треугольники, их придётся разрезать на более мелкие. Сделать это не так-то просто.
В настоящее время вместо сортировки треугольников используется более быстрый способ определения порядка треугольников - буфер глубины.
Буфер глубины в DirectX
Буфер глубины - это точно такая же поверхность, как и фоновый буфер. Причём размер буфера глубины по количеству элементов всегда равен размеру фонового буфера/основной поверхности.
В буфере глубины хранятся z координаты пикселей (!!!) треугольников. По этой причине буфер глубины также называют и z-буфером.
Важно понимать, в какой момент заполняется z-буфер.
После преобразования в проекционную плоскость (это не совсем плоскость, скорее это половина куба), значения вершин по x и y расположены между -1 и 1. Значения z находятся на отрезке от нуля до единицы.
Как мы выяснили в прошлом уроке, далее происходит переворачивание картинки (умножение y координаты всех вершин на -1) и "растягивание" картинки по вертикали и горизонтали до размера окна. После этого x и y координаты всех вершин будут находиться в диапазоне от нуля до ширины/высоты. Заметьте, здесь x, y координаты уже будут целыми числами (в проекционной плоскости они были дробными), потому что это уже координаты пикселей в окне. Нельзя обращаться к пикселям как 25,5 или 126,0023, только 25 или 126. В данный момент ещё не существует никаких треугольников, только вершины, которые описывают эти треугольники. При этом координаты z этих вершин всё это время остаются неизменными - они расположены на отрезке [0, 1].
Далее происходит создание треугольников: между вершинами проводятся рёбра, а затем все пиксели внутри закрашиваются цветом. При этом для каждого пикселя вычисляется значение z.
После этого происходит заполнение z-буфера. Сначала все его элементы устанавливаются в максимальное значение. Так как z координаты вершин берутся из проекционной плоскости, то максимальным значением будет единица. Если бы z координаты брались из пространства камеры (можно реализовать и такой вариант), то максимальное значение было бы равно z-координате дальней отсекающей плоскости.
И только теперь начинают проверяться все пиксели каждого треугольника. В буфер глубины заносится минимальное значение z - этот пиксель находится ближе всего к экрану и он закрывает все пиксели, расположенные за ним.
Как мы видим, данный способ требует очень много вычислений: проверяется каждый пиксель каждого треугольника. И этот способ был бы медленнее сортировки треугольников, если бы он выполнялся программно, а не аппаратно.
Программные и аппаратные вычисления (Software and hardware rendering)
Когда в методе IDirect3D9::CreateDevice мы указываем второй параметр как D3DDEVTYPE_HAL, это значит что растеризация (а вместе с ней и все вычисления связанные с буфером глубины), будет выполняться аппаратно. Если передаётся D3DDEVTYPE_REF, то вычисления будут производиться программно.
Чем отличаются программные вычисления от аппаратных? Центральный процессор компьютера может производить любые вычисления (на самом деле большую часть времени процессор занят выполнением простейших арифметических операций). Когда мы в программе "Камера" самостоятельно производили преобразования, все вычисления происходили программно, т.е. для этого была написана специальная программа (функции multiplication и transformation). Затем эту программу выполнял центральный процессор. Но в DirectX есть возможность производить преобразования аппаратно. Для этого нужно задать матрицы преобразования через SetTransform. При программных вычислениях всё делает центральный процессор. При аппаратных, вычислениями занимается отдельное физическое устройство, специально созданное для данной конкретной задачи. Но тут есть одна проблема: если в видеокарте нет аппаратной поддержки какой-нибудь функции (например, z-буфера или преобразований), то всё придётся делать центральному процессору программно.
Аппаратные вычисления всегда быстрее программных, так как при аппаратных вычислениях существует специальное, оптимизированное под конкретную задачу, физическое устройство (группа транзисторов).
Большинство современных видеокарт обеспечивают аппаратные преобразования, z-буфер и много чего ещё. Для использования этих возможностей, устройства IDirect3DDevice9 нужно создавать с флагом D3DDEVTYPE_HAL. В противном случае, всей графической частью будет заниматься центральный процессор.
Использование z-буфера в DirectX
Для первой картинки урока я использовал старую программу, где преобразования осуществлялись автоматически, а матрицы преобразования задавались через SetTransform.
В структуре D3DPRESENT_PARAMETERS для z-буфера выделено два поля:
BOOL EnableAutoDepthStencil; D3DFORMAT AutoDepthStencilFormat;
Первое поле позволяет включить (enable) использование буфера глубины для данного устройства. Второй флаг задаёт формат буфера глубины. В настоящее время в буфере глубины вместе хранятся и z-буфер и трафаретный (stencil) буфер. О трафаретном буфере мы будем говорить позже.
Форматы буфера глубины могут быть следующими (это не все форматы):
D3DFMT_D16_LOCKABLE
16-битный замыкаемый буфер глубины.
D3DFMT_D32
32-битный буфер глубины.
D3DFMT_D15S1
16-битный буфер глубины. 15 бит - на канал z-буфера, 1 бит на канал трафаретного буфера.
D3DFMT_D24S8
32-ухбитный буфер глубины. 24 бита - z-буфер, 8 бит - трафаретный буфер.
D3DFMT_D24X8
32-ухбитный буфер глубины. 24 бита - z-буфер, 8 бит не используется.
D3DFMT_D24X4S4
32-битный буфер глубины. 24 бита - z-буфер, 4 бита не используется, 4 бита - трафаретный буфер.
Мы будем использовать D3FMT_D24S8. Пока что канал трафаретного буфера будет пустовать. Теперь заполнение структуры D3DPRESENT_PARAMETERS будет выглядеть так:
D3DPRESENT_PARAMETERS pp; ZeroMemory(&pp,sizeof(pp)); pp.BackBufferWidth = 500; pp.BackBufferHeight = 500; pp.BackBufferFormat = D3DFMT_X8R8G8B8; pp.BackBufferCount = 1; pp.MultiSampleType = D3DMULTISAMPLE_NONE; pp.SwapEffect = D3DSWAPEFFECT_DISCARD; pp.hDeviceWindow = hWnd; pp.Windowed = true; pp.EnableAutoDepthStencil = 1; pp.AutoDepthStencilFormat = D3DFMT_D24S8; pp.Flags = D3DPRESENTFLAG_LOCKABLE_BACKBUFFER;
При создании фоновой и основной поверхности, Direct3D создаст и буфер глубины. При этом количество элементов в фоновой/основной поверхности равно количеству элементов в буфере глубины. Больше ничего делать не нужно, DirectX всё остальное сделает за нас. Единственное, каждый кадр необходимо очищать буфер глубины (так же как и фоновый буфер). Для этого используется уже знакомый нам метод Clear:
HRESULT Clear( DWORD Count, CONST D3DRECT * pRects, DWORD Flags, D3DCOLOR Color, float Z, DWORD Stencil );
Третий параметр - набор флагов: D3DCLEAR_STENCIL, D3DCLEAR_TARGET, D3DCLEAR_ZBUFFER. Так как нам нужно очищать фоновый буфер и буфер глубины, то указывать мы будем два последних флага. Предпоследний аргумент - значение z, которым будут проинициализированы все элементы z-буфера. Сюда нужно передавать максимальное значение z-буфера - единицу. Теперь пример вызова метода Clear:
dev->Clear(0, NULL, D3DCLEAR_TARGET | 3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(255,255,255), 1.0f, 0);
Вот и всё.
Состояния рендеринга z-буфера
Для управления поведением z-буфера доступно несколько типов состояний:
D3DRS_ZENABLE
Позволяет включить/выключить z-буфер. Возможны три варианта: D3DZB_FALSE - z-буфер выключен, D3DZB_TRUE - z-буфер включён, D3DZB_USEW - вместо z-буфера использовать w-буфер. По умолчанию используется состояние в зависимости от значения поля EnableAutoDepthStencil структуры D3DPRESENT_PARAMETERS.
D3DRS_ZWRITEENABLE
Тип состояния разрешает запись в z-буфер. По умолчанию установлено TRUE. Если установлено значение FALSE, то пикселю будут тестироваться на значение z, но запись новых значений в z-буфер запрещена. Практически никогда не используется.
D3DRS_ZFUNC
Этот тип состояний позволяет изменить способ, каким сравниваются пиксели. По умолчанию значение z пикселя сравнивается со значением z буфера глубины, и если у пикселя z-значение меньше или равно, то в z-буфер записывается новое значение. В данном случае используется значение D3DCMP_LESSEQUAL (less - меньше, equal - равно). Т.е. данный тип состояний позволяет изменить функцию сравнения значений глубины пикселя и z-буфера. Кроме D3DCMP_LESSEQUAL другие значения практически никогда не используются на практике.
w-буфер и z-буфер
Значения в z-буфере зависят от отношения дальней и ближней отсекающих плоскостей. При определённых z координатах этих плоскостей, значения в z-буфере могут распределяться неравномерно: большой диапазон значений будет сосредоточен близко от игрока. Подобные ситуации часто возникали раньше, если в игре были открытые пространства и использовался 16-битный z-буфер. В таких случаях на ближнем расстоянии всё отображалось нормально, но на дальнем расстоянии треугольники могли рисоваться не в правильном порядке.
Для решения этой проблемы использовался w-буфер. Т.е. вместо z координаты проверялась однородная. Не во всех видеокартах есть поддержка w-буфера. Сейчас, когда в большинстве случаях используется 24-битные или даже 32-ухбитные z-буферы, использовать w-буфер не имеет смысла.
Проверка поддержки форматов z-буфера
Как мы выяснили, z-буфер требует довольно больших вычислительных мощностей. В реальных программах z-буфер обязательно должен создаваться аппаратно. Но не все видеокарты поддерживают все форматы буфера глубины. Например, моя видеокарта не поддерживает D3DFMT_D32. В DirectX есть возможность проверить, какие форматы буфера глубины (и других поверхностей) поддерживает видеокарта. Для этого используется метод IDirect3D9::CheckDeviceFormat. Этот метод нужно вызывать до заполнения структуры D3DPRESENT_PARAMETERS:
HRESULT CheckDeviceFormat( UINT Adapter, D3DDEVTYPE DeviceType, D3DFORMAT AdapterFormat, DWORD Usage, D3DRESOURCETYPE RType, D3DFORMAT CheckFormat );
UINT Adapter
Первый параметр указывает, какой графический адаптер будет проверяться. Здесь возможны разные значения, если на компьютере расположено больше одной видеокарты. Мы будем передавать 0 или D3DADAPTER_DEFAULT (это одно и то же).
D3DDEVTYPE DeviceType
Тип устройства. Практически всегда передаётся D3DDEVTYPE_HAL. Этот тип устройства говорит, что будет проверяться аппаратная поддержка.
D3DFORMAT AdapterFormat
Формат вывода изображения адаптера. Сюда передаётся формат фоновой/основной поверхностей.
DWORD Usage
Параметр, указывающий, каким образом используется поверхность, формат которой проверяется. Так как мы проверяем буфер глубины, то передавать нужно D3DUSAGE_DEPTHSTENCIL. Другие возможные значения вы можете посмотреть в документации: Direct3D Reference → Constants → D3DUSAGE.
D3DRESOURCETYPE RType
Тип ресурса. Мы проверяем поверхность (буфер глубины - это поверхность), поэтому значение будет D3DRTYPE_SURFACE. Другие значения D3DRESOURCETYPE можно посмотреть в справке: Direct3D Reference → Enumerations → D3DRESOURCETYPE.
D3DFORMAT CheckFormat
Формат, который нужно проверить.
Вот пример проверки аппаратной поддержки формата D3DFMT_D24S8:
HRESULT hr = 0; hr = CheckDeviceFormat(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, D3DFMT_X8R8G8B8, D3DUSAGE_DEPTHSTENCIL, D3DRTYPE_SURFACE, D3DFMT_D24S8); if (hr != S_OK) { // формат не поддерживается }
Если формат не поддерживается, то функция вернёт значение отличное от S_OK.
Ещё одно замечание по поводу S_OK. Когда мы проверяли ввод в DirectInput, для проверки успешного выполнения функции мы использовали константу DI_OK. В Direct3D есть своя константа, которая проверяет успешное завершение функции - D3D_OK. Все три константы: S_OK, DI_OK, D3D_OK - имеют одно и то же значение - ноль. Можно использовать любую.
И последнее: интерфейс IDirect3D может проверить не только поддержку форматов поверхностей, но и много других вещей. Более подробно - на странице с документацией по интерфейсу IDirect3D. Перед созданием устройства крайне желательно проверить аппаратную поддержку всех возможностей, которые будут использоваться в программе.
Заключение
Сегодня мы познакомились с двумя способами вывода треугольников в правильном порядке.
Сортировка треугольников довольно эффективный и легко реализуемый программно метод. Сложные ситуации, когда несколько треугольников пересекаются каким-либо причудливым образом, возникают редко и их можно даже не принимать в расчёт. Если бы не было более быстрого способа, то можно было бы обойтись сортировкой треугольников.
z-буфер реализован аппаратно, поэтому он быстрее сортировки треугольников. Помимо этого он легко справляется с трудными ситуациями, когда треугольники пересекаются в пространстве. К тому же z-буфер проще реализовать аппаратно. При сортировке происходит перемещение данных, а в z-буфере используются примитивные тесты: z-значение одного пикселя меньше или равно z-значению другого пикселя.
На сегодня всё. До скорой встречи.