Отметим, что использование функции saturate(d * 9999), которая пропускает все положительные аргументы, даже очень маленькие, полностью блокирует луч. Однако, значения глубоко внутри поверхности имеют нарастающие значения функции плотности, а значения, удаленные от поверхности – более отрицательные. Хотя функция плотности не есть строго знаковая функция расстояния, но она часто подходит, поэтому мы используем ее преимущество.
Пример 1-2. Псевдокод генерации Ambient Occlusion для вершины
- float visibility = 0;
- for (ray = 0 .. 31)
- {
- float3 dir = ray_dir[ray]; // Из буфера констант
- float this_ray_visibility = 1;
- // Короткие выборки из объема плотности:
- for (step = 1 .. 16) // Не начинаие с нуля!
- {
- float d = density_vol.Sample( ws + dir * step );
- this_ray_visibility *= saturate(d * 9999);
- }
- // Длинные выборки функции плотности:
- for (step = 1 .. 4) // Don't start at zero!
- {
- float d = density_function( ws + dir * big_step );
- this_ray_visibility *= saturate(d * 9999);
- }
- visibility += this_ray_visibility;
- }
- return (1 – visibility/32.0); // Возваращение occlusion
float visibility = 0;
for (ray = 0 .. 31)
{
float3 dir = ray_dir[ray]; // From constant buffer
float this_ray_visibility = 1;
// Short-range samples from density volume:
for (step = 1 .. 16) // Don't start at zero
{
float d = density_vol.Sample( ws + dir * step );
this_ray_visibility *= saturate(d * 9999);
}
// Long-range samples from density function:
for (step = 1 .. 4) // Don't start at zero!
{
float d = density_function( ws + dir * big_step );
this_ray_visibility *= saturate(d * 9999);
}
visibility += this_ray_visibility;
}
return (1 – visibility/32.0); // Returns occlusionВ процессе отбора лучей, вместо прямой интерпретации объекта экземпляра как черного или белого (попадание или промах), мы рассматриваем "размытие". Частичное преграждение случается, когда объект находится рядом с поверхностью (или не так глубоко). На демо на DVD, мы используем множитель 8 (лучше чем 9999) для близких объектов, и 0.5 для далеких. (Заметим, что эти значение родственны к диапазону значений, которые мы получаем при помощи функции плотности). Эти небольшие множители благоприятны для удаленных образцов; становится трудно сказать, что были взяты всего 4 образца. Рисунки 1-18 - 1-20 приводят несколько примеров.
Рисунок 1-18 Только удаленный Ambient Occlusion
Figure 1-19 Удаленный и близкий Ambient Occlusion
Figure 1-20 Сцена, затененная при помощи Ambient Occlusion.
1.4.2 Создание блока: Метод 1
Этот раздел рассматривает три способа создания блока. От метода 1 к методу 3 способ будет становиться более сложным, но при этом, более быстрым.
Первый (и простейший) метод построения блока поверхности самый простой и требует только двух проходов, как показано в Таблице 1-1.
Таблица 1-1. Первый метод создания блока
Имя прохода Описание Выход геометрического шейдера
build_densities Заполнение объема значениями плотности N/A
gen_vertices Посещение всех (неграничных) вокселей в заданном объеме.
Геометрический шейдер выдает до 15 вершин (5 треугольников) на вокссель.
- float4 wsCoordAmbo;
- float3 wsNormal;
Число: 0/3/6/9/12/15
К тому же GS не так быстр, как VS потому что он более гибок и лучше совместим. Переложение работы создания вершин, особенно обработки лучей ambient occlusion, на вершинный шейдер принесет значительную пользу. К счастью, мы можем уменьшить выход GS, добавив еще один проход.Однако, этот метод легко оптимизируем. Во-первых, скорость геометрического шейдера уменьшается с увеличением максимального размера выхода (в расчете на один входной примитив). Здесь наш максимальный выход равен 15 вершинам, состоящим из 7 дробных чисел—всего 105 чисел. Если мы сможем уменьшить их количество до 32 или меньше—или даже 16 или меньше—GS будет работать гораздо быстрее. Геометрический шейдер выдает до 15 вершин (5 треугольников) на вокссель.
- float4 wsCoordAmbo;
- float3 wsNormal;
Число: 0/3/6/9/12/151.4.3 Создание блока: Метод 2
Проблема метода 1—очень большой размер выхода геометрического шейдера (на один вход) и необходимость переложить работу с геометрического шейдера на вершинный—как показано в Таблице 1-2, что дает 22х кратный прирост по сравнению с методом 1.Таблица 1-2. Второй метод создания блока
Имя прохода | Описание | Выход геометрического шейдера |
build_densities | Заполнение объема значениями плотности | N/A |
list_triangles | Перебрать все воксели в заданном объеме; вывести легковесный маркер для каждого создаваемого треугольника. Исользовать очередь, чтобы пропустить ненужные проходы | uint z6_y6_x6_edge1_edge2_edge3; Число: 0–5 |
gen_vertices | Пройти вершинный шейдером по списку треугольников и сделать большинство работ по созданию вершин. Геометрический шейдер нужен только для вывода результата в буфер |
Число: 3 |
Ключевая информация для генерации каждого треугольника упакована в uint:
- struct triangle_marker_point {
- uint z6_y6_x6_edge1_edge2_edge3;
- };
struct triangle_marker_point {
uint z6_y6_x6_edge1_edge2_edge3;
};Шесть целых, упакованные в один uint, хранят все, что нужно для построения треугольника внутри вокселя. Битовые поля x, y, и z (6 бит на каждое, или [0..31]) показывают, в каком вокселе внутри блока лежит данный треугольник. И три поля вершин (каждое 4 бита) хранят ребро [0..11] на котором вершина должна быть расположена. Эта информация, плюс доступ к значениям плотности - все что нужно геометрическом шейдеру для создания трех вершин, образующих треугольник. На последнем проходе, все три вершины выполняются одним вызовом вершинного шейдера и затем передаются геометрическому вместе, в большой структуре:
- struct v2gConnector {
- float4 wsCoordAmbo1;
- float3 wsNormal1;
- float4 wsCoordAmbo2;
- float3 wsNormal2;
- float4 wsCoordAmbo3;
- float3 wsNormal3;
- };
struct v2gConnector {
float4 wsCoordAmbo1;
float3 wsNormal1;
float4 wsCoordAmbo2;
float3 wsNormal2;
float4 wsCoordAmbo3;
float3 wsNormal3;
};Далее GS выбирает из структуры 3 вершины. Эта процедура создает список треугольников аналогично списку в Методе 1, но гораздо быстрее.
Добавление еще одного прохода полезно потому что мы можем не выполнять последний проход (самый трудоемкий) если мы поняли, что в данном блоке нет треугольников. Для определения, были ли получены треугольники, достаточно сделать проход list_triangles с очередью выхода (ID3D10Query с D3D10_QUERY_SO_STATISTICS), которая вернет количество своих элементов. Это еще одна причина, по которой мы получили такой большой прирост скорости.
Метод 2 быстрее за счет идеи переложения тяжелой работы с GS на VS. Однако, метод 2 имеет один недостаток: он создает каждую вершину для каждого треугольника, к которому она принадлежит. Вершина обычно принадлежит пяти треугольникам, то есть мы делаем в 5 раз больше работы, чем должны.
1.4.4 Создание блока: Метод 3
Этот метод создает каждую вершину единожды, вместо пяти раз, как в предыдущем методе. Несмотря на еще один проход, метод 3 быстрее метода 2 на 80%. Метод 3, вместо создания простого ненумерованного списка треугольников в множестве вершин (многие из которые лишние), создает раздельно множество вершин и список индексов. Индексы из списка ссылаются на вершины, и каждые три индекса описывают треугольник.Для того, чтобы обрабатывать каждую вершину единожды, мы ограничим создаваемые вершины внутри ячейки ребрами 3, 0, и 8. Вершина, лежащая на других ребрах будет создана другой ячейкой—одна из совпадающих вершин попадет на ребро 3, 0, или 8. Благодаря этом каждая нужная вершина будет произведена один раз.
Внутри ячейки легко проверить лежит ли вершина на нужном на ребре (3, 0 или 8), определяя различаются ли биты экземпляра на концах ребра. В качестве примера, можно посмотреть на файл shaders\4_list_vertices_to_generate.vsh, но надо отметить, что это проще сделать с помощью таблиц.
Проходы для создания уникальных вершин показаны в Таблице 1-3.
Таблица 1-3. Метод 3 создания блока
|
Проблема при создании индексов для данной (непустой) ячейки в том, что мы не знаем, где находятся вершины буфере вершин, который магическим образом хранит структуру, в которой объединены все вершины. (Отметим, что только очень малая часть ячеек создает вершины.) Наше решение - 3D текстура; мы используем ее для хранения ID вершины (или индекса внутри буфера вершин) в структуре, к который имеем произвольный доступ. Функция выборки может работать как функция, принимающая на вход 3D позицию (внутри блока) и выдающая ID вершины (или индекс внутри буфера вершин), которая должна быть получена здесь. Эта та недостающая информация, которая нужна нам для получения списка вершин и для объединения их в треугольники. Два дополнительных прохода описаны в Таблице 1-4.
Таблица 1-4. Метод 3 для создания индекса буфера
Имя прохода | Описание | Выход геометрического шейдера |
splat_vertex_ids | Перебор vert_list и сохранение каждого SV_VertexID в VertexIDVol. | (Нет вывода; пиксельный шейдер выписывает SV_VertexID.) |
gen_indices | Перебор nonempty_cell_list и вывод до 15 элементов на ячейку—индексы, описывающие, до 5 треугольников. Выборка VertexIDVol для получения этой информации. | uint index; Число: 15 Замечание: Не выводить индексы для ячеек в каждом из последних рядов (в x/y/z). |
Когда выбирается ID вершины, нужно иметь ввиду, что если вам нужен ID вершины, лежащей на ребре, отличном от 3, 0, или 8, вместо этого надо выбрать соседний воксель на ребрах 3, 0, или 8. Механизм этого довольно прост; смотрите shaders\7_gen_indices.gsh на DVD.
Надо иметь ввиду, что нам часто придется записывать до 3х вершина на воксель, но мы пишем только в одноканальную текстуру! Поэтому, если ячейка имеет вершины на ребрах 3 и 8, в VertexIDVol можно записать только один индекс. Простое решение – утроить размер хранилища для ID вершин (сделать размер 3NxNxN). При записи, надо умножать координату x на 3, и добавлять 0, 1, или 2 в зависимости от ребра, на котором находится вершина (3, 0, или 8). (Смотри shaders\5b_splat_vertex_IDs.vsh.) На последнем проходе, когда вы выбираете результаты, преобразование координаты x происходит аналогично, в зависимости от того, на каком ребре 3, 0, или 8 находится ID вершины внутри вокселя. (Смотри shaders\7_gen_indices.gsh.)
Также отметим, что в методе 3, проход list_nonempty_cells проход включает дополнительные ячейки на концах x, y и z осей. В проходе gen_indices, который оперирует с nonempty_cell_list, индексы не создаются для элементов за пределами блока. (Смотри shaders\7_gen_indices.gsh для примера.)
Метод 3 позволяет хранить вершины и иметь к ним общий доступ, и как результат, создавать только одну пятую из них. Это сильно ускоряет процесс, учитывая что каждая вершина выбирает функцию плотности 128 раз (32 лучей x 4 удаленного occlusion на луч).
Используя функцию плотности, NVIDIA GeForce 8800 GPU может создать 6.6 блоков в секунду, используя метод 1, около 144 блоков, используя метод 2, и около 260 блоков в секунду, используя метод 3.
1.5 Текстурирование и затенение
Наша задача процедурной генерации поверхности, или другой формы случайной топологии, это текстурирование—особенно, создание текстурных координат, также известных как UV. Как мы можем преобразовать бесшовные тексутры к полигонам? Простая плоская проекция, которая показана на Рисунке 1-21, бесшовно повторяющейся 2D текстуры хорошо выглядит только под одним углом, но плохо с других, потому что она сильно растягивается (смотри также Рисунок 1-23).Рисунок 1-21 Простая плоская проекция, подверженная искажениям
Простой способ исправить это – трехпроекционное текстурирование, или 3 разные плоские проекции, каждая вдоль соответствующей оси (x, y, и z). В каждой точке, мы используем проекцию, которая имеет меньше искажений (растягиваний) в этой точке—с другими проекциями, смешивающимися в граничных зонах, как на Рисунке 1-22. Например, в точке поверхности, чей вектор нормали направлен больше вдоль оси x, нужно использовать yz плоскую проекцию. Уровень смешивания приблизительно от 10 до 20 градусов подходит хорошо; смотри Рисунок 1-23 для иллюстрации, насколько высок уровень смешивания .
Рисунок 1-22 Три плоские проекции одной текстуры, смешанные на основании вектора нормали
Рисунок 1-23 Трехпроекционное текстурирование
Комментариев нет:
Отправить комментарий