Я задумался над оптимизацией early Z-pass с точки зрения нагрузки на CPU. Сразу необходимо отметить, что я не рассматриваю варианты, где возможно применение инстансинга, речь пойдёт о выводе различной геометрии с топологией triangle list. Ключевым преимуществом является то, что в early Z-pass изменения render states минимальны, типичный сценарий отрисовки примерно такой:
for each objectЛегко видеть, что между вызовами Draw() меняются только константы (матрица WorldViewProj) и вершинный/индексный буферы. Это можно оптимизировать, если объединить все геометрические данные в одном буфере. В Direct3D 10/11 с помощью параметров StartIndexLocation и BaseVertexLocation метода ID3D11DeviceContext::DrawIndexed() можно управлять, какую порцию вершинных данных необходимо отрисовать. Тогда оптимизированный вариант может выглядить так:
{
UpdateMatrix();
SetVertexBuffer();
SetIndexBuffer();
DrawGeometry();
}
SetBigVertexBuffer();Как мешают эти матрицы! А что, если обновлять матрицы не поодиночке, а сразу массив матриц для всех объектов? Тогда возникает вопрос, каким образом применить трансформацию индивидуально к каждому объекту? К сожалению, в Direct3D из шейдера невозможно узнать номер вызова отрисовки (по примеру SV_InstanceID в режиме инстансинга), но можно ценой дополнительных усилий организовать это вручную. Допустим, нам нужно отрисовать три объекта, каждый из которых имеет 3, 4 и 8 вершин. Создадим дополнительный вершинный буфер, атрибуты которого содержат ID объекта. Если мы объединим геометрию последовательно, то дополнительный буфер должен иметь вид:
SetBigIndexBuffer();
for each object
{
UpdateMatrix();
DrawGeometry();
}
000111122222222Если подсоединить этот буфер на дополнительный IA-слот, то в шейдере станет возможным выбирать матрицу трансформации по ID из этого буфера. Теперь оптимизированный вариант рендеринга будет выглядить так:
UpdateMatricesForAllObjects();В итоге, в цикле остались только непосредственно вызовы метода рисования. Direct3D runtime оптимизирован в том смысле, что DIP cost зависит от количества изменений на конвейере, т. е. чем меньше изменений с момента предыдущего DIP, тем дешевле следующий за ним. В последнем варианте наиболее тяжёлым будет только первый вызов, все остальные - значительно легче (насколько именно - зависит от вендора и версии драйвера).
SetBigVertexBuffer();
SetMeshIDVertexBuffer();
SetBigIndexBuffer();
for each object
{
DrawGeometry();
}
Присутствие цикла объясняется тем, что в кадре обычно видна только часть всех объектов сцены, поэтому необходимо определять их видимость из виртуальной камеры, заносить в отдельный список и рисовать объекты только из этого списка. Если специфика игры такова, что все объекты видны в камере, цикл можно заменить единственным(!) вызовом, подготовив отдельный буфер индексов и указав IndexCount для всех объектов. Если от цикла нельзя избавиться, можно попробовать сократить кол-во его итераций: например, если какие-то объекты видны в камере и одновременно лежат в буфере линейно, вызовы отрисовки для каждого из них можно объединить в один (этакий DIP-merge).
Для практической проверки всех этих мыслей на коленке была написана демка. Она показывает четыре объекта с разной геометрией, каждый из которых имеет свою трансформацию:
Ссылка на демо: dipreduc.exe
P.S. Поразмышлять на тему оптимизации меня побудил следующий вброс: DX11 vs GL Driver Overhead
где просто предлагается сделать 100000 DIP-ов чтобы прочувствовать мощь расширений NVidia :)