Почти все слышали про SSAO. Эта техника позволяет рассчитать примерное значение затенения от глобального рассеянного освещения на основе глубины и используется в реальном времени в шейдерах.
Но если вы делаете трёхмерное приложение для веб или широкого круга пользователей, если важно сохранить быстродействие приложения, тогда можно заранее рассчитать освещение/затенение в текстуру. С Ambient Occlusion это тоже работает.
Лайтмап (shadowmap) - текстура, каждая точка в которой имеет собственную трёхмерную координату на сцене. Натянув эту текстуру на сцену, мы получим значение освещённости или затенения на каждом полигоне трёхмерного пространства.
Примерный процесс построения
Выбираем масштаб, разрешение нашей лайтмапы, сколько точек освещения поместится на полигоне размером 1х1.
Полигон плоский, поэтому заведём для каждого полигона Rect - в нём будет нарисовано освещение для этого конкретного полигона. Каждая точка полигона имеет координату в прямоугольнике. А т.к. полигон расположен в пространстве, то каждой точке в прямоугольнике мы можем сопоставить 3D координату.
Используя алгоритмы Bin Packing, укладываем эти прямоугольники на большую текстуру как можно плотнее. Это ключевой момент, мы получили соответствие "3D точка в пространстве - 2D точка в лайтмапе".
В качестве бонуса. Бросаем луч из источника света в эту точку, проверяем пересечение этого луча с другими полигонами, получаем значение освещённости данной точки. Записываем это значение в лайтмап.
Построив лайтмап, рассчитав освещение от каждого источника света, мы можем придать сцене глубины, вычислив для каждой точки лайтмапы значение затенения от рассеянного света - Ambient Occlusion. В каноническом варианте используется метод Монте-Карло: нам необходимо бросить N лучей из точки и вычислить, сколько из них пересекаются с существующей геометрией.
Значение AO можно будет посчитать по формуле:
Альтернативный воксельный метод
Имея список точек, мы можем вычислить затенение более честно, учитывая сумму минимальных расстояний из конкретной точки до других точек, которые присутствуют в лайтмапе.
Для этого понадобится какая-то функция, преобразующая направление в значение минимального расстояния. Можно использовать трёхмерную матрицу, центральная клетка которой будет означать нашу точку, а значения на сфере внутри матрицы мы будем заполнять минимальным расстоянием в направлении данной ячейки. Для нахождения нужного вокселя в матрице нужно будет просто нормализовать вектор луча и разместить по центру:
ivec3 getCoord(vec3 point, vec3 other, ivec3 matrixSize) {
vec3 dir = other - point;
vec3 norm = normalize(dir);
vec3 matrixHalf = vec3(matrixSize) * 0.5;
return matrixHalf + ivec3(norm * matrixHalf);
}
Далее нам будет необходимо заполнить такую матрицу для каждой точки. Процесс заполнения в двумерном случае выглядит примерно так:
Вспомнив о том, что скалярное произведение разнонаправленных векторов меньше 0, можно сразу отсекать точки, находящиеся с другой стороны от плоскости, на которой лежит текущая точка.
Далее мы можем вычислить AO в данной точке, взяв среднее значение всей матрицы. При этом некоторые ячейки заполнены бесконечностью, и вместо неё нужно использовать максимальную дистанцию Ambient Occlusion.
В качестве заключения
Данный метод вычислительно сложнее, чем пускать N лучей и проверять коллизии с геометрией. Время расчёта имеет прямую зависимость от квадрата количества точек, тогда как время расчёта AO каноническим методом зависит от количества точек, помноженного на количество полигонов. Но, субьективно, результат получается лучше. По сути это Ray Tracing с шагом 1, в котором участвуют точки не из фреймбуфера, а из лайтмапы.
В следующей статье я расскажу о самом быстром способе запекания Ambient Occlusion.