Чтобы не считать честно распределение плотности тумана для каждого пикселя, как обычно, вводят некоторую аппроксимацию, что в нашем случае приводит к понятию "вертексного тумана" (vertex fog). Т.е. мы рассчитываем распределение тумана для вершин полигонов находящихся в объеме тумана, а затем полученные значения интерполируем по поверхности полигона.
Программеры Unreal решили, что линейной интерполяции им будет недостаточно и что обязательно нужно использовать честные формулы. В результате в Анрыле мы имеем так называемые "карты тумана" (fog maps) - дополнительные текстуры, которые аддитивно накладываются на полигоны и создают тем самым эффект тумана. Основная фишка фогмэпов состоит в том, что их нужно
перерассчитывать каждый кадр, что влечет за собой громоздкие вычисления (несколько тысяч линейных интегралов по объему каждый кадр), поэтому чтобы хоть как-то сэкономить время, фогмэпы рассчитываются в небольших разрешениях и накладываются с помощью дополнительных конвейеров мультитекстурирования.
Кармак поступил намного проще, для отрисовки тумана в третьем Квейке он воспользовался ни чем иным, как обыкновенной заливочкой Гуро, которая хоть и не является перспективно-корректной, но, тем не менее, позволяет добиться неплохих результатов малой кровью. Рассмотрим данный метод подробнее.
Чтобы совсем не загрузиться математикой прибегнем к упрощениями, которыми пользовался Кармак. Во-первых объем тумана, должен быть параллелепипедом ориентированным так, чтобы его стороны были перпендикулярны координатным осям, а во-вторых, содержимое объема тумана может быть видно только через одну грань этого объема. Если подобные ограничения тебя не устраивают, уверен, ты сможешь их обойти 😉
Допустим наша сцена состоит из одного браша заполненного туманом и нескольких брашей геометрии:
Чтобы корректно отобразить туман в данном случае нам придется разбить наши браши, пересекающие объем тумана, на две части: одну, лежащую внутри тумана, и вторую лежащую снаружи, это можно сделать заранее, а потом использовать уже разбитые полигоны.
Теперь для каждой вершины полигонов лежащих внутри тумана мы должны вычислить контрибуцию тумана, т.е. найти длину части отрезка, соединяющего точку наблюдения с вершиной полигона, которая лежит внутри тумана, и выполнить соответствующие вычисления для определения цвета, вносимого туманом.
Так как мы можем видеть вершину лежащую в тумане, только через один заранее определенный полигон браша ограничивающего туман, то все что нам нужно сделать, так это найти координаты точки пересечения нашего луча с плоскостью выбранного полигона. Сделать это можно с помощью следующей функции.
// структура описывающая плоскость
typedef struct {
float normal[3]; // нормаль к поверхности (A, B, C)
float dist; // dist = A*x + B*y + C*z, где (x, y, z) - это
// координаты любой точки принадлежащей плоскости
}
// вертекс (x, y, z)
typedef float vec3_t[3];
// p - плоскость пересечение с которой находим;
// start, end - начальные и конечные координаты луча;
// out - искомая точка пересечения плоскости и луча;
//
int GetIntersectionPoint (plane_t p, vec3_t start, vec3_t end, vec3_t out)
{
float dist1, dist2, dot;
// найдем расстояние от start до плоскости
dist1 = start[0]*p.normal[0] + start[1]*p.normal[1] + start[2]*p.normal[2];
dist1 -= p.dist;
// найдем расстояние от end до плоскости
dist2 = end[0]*p.normal[0] + end [1]*p.normal[1] + end[2]*p.normal[2];
dist2 -= p.dist;
// если луч не пересекает плоскость, то выходим
if (dist1*dist2 > 0) return 0;
// найдем точку пересечения
dot = dist1 / (dists1 - dist2);
out[0] = start[0] + dot*(end[0] - start[0]);
out[1] = start[1] + dot*(end[1] - start[1]);
out[2] = start[2] + dot*(end[2] - start[2]);
return 1;
}
Теперь зная координаты точки пересечения луча с брашем тумана, мы без труда можем найти вклад тумана в общее освещение фрагмента.
// fogcolor - цвет тумана;
// density - плотность тумана, т.е. минимальная толщина слоя
// тумана, когда он становится непрозрачным;
// start - точка вхождения луча в браш тумана;
// end - вершина в тумане к которой проводили луч;
// color - результирующий цвет;
//
void CalcVertexFogging (vec3_t fogcolor, float density, vec3_t start, vec3_t end, vec3_t color)
{
float dist, alpha;
// найдем расстояни между точками - путь луча в тумане
end[0] -= start[0];
end[1] -= start[1];
end[2] -= start[2];
dist = sqrt (end[0]*end[0] + end[1]*end[1] + end[2]*end[2]);
// рассчитаем прозрачность тумана
alpha = min(1.0, dist/density);
// и результирующий цвет
color[0] = alpha * fogcolor[0];
color[1] = alpha * fogcolor[1];
color[2] = alpha * fogcolor[2];
}
Найдя подобным способом цветовые компоненты тумана для всех вершин полигона, остается только аддитивным блендингом закрасить полигон, используя полученные цвета.
Графические API и вертексный туман
Вертексный туман официально поддерживается такими низкоуровневыми графическими API как DirectX и OpenGL. Для этого, к атрибутам вершин: координатам, цвету, координатам текстуры, координатам нормали и т.д. был добавлен еще один атрибут - координата тумана (fog coordinate), которая позволяет непосредственно задать значение плотности тумана для каждой вершины. И затем, при построении изображения, API использует эту координату и текущие установки тумана (цвет, плотность) для отрисовки полигонов с необходимыми цветовыми изменениями, вносимыми туманом.
В OpenGL использование координат тумана возможно только для драйверов поддерживающих расширение GL_EXT_fog_coord. Перспективная коррекция для тумана, равно как и для текстур контролируется параметром GL_PERSPECTIVE_CORRECTION_HINT. Прототип функции для задания значения координаты тумана имеет следующий вид: void glFogCoord[fd]EXT (T coord);
Для DirectX инициализация вертексного тумана осуществляется практически так же, как и пиксельного, нужно только для метода SetRenderState, вместо константы D3DRS_FOGTABLEMODE передать D3DRS_FOGVERTEXMODE. А чтобы самому задать значение плотности тумана для вертекса, нужно вычислить прозрачность тумана, как это показано выше, и поместить полученное значение в альфа-компоненту отраженного (specular) цвета вершины.
Вычисления и рендеринг вертексного тумана для карточек, поддерживающих модуль T&L, выполняется аппаратно, что нельзя не поприветствовать бурными аплодисментами. Ура, товарищи!