ОСВЕЩЕНИЕ
5.5. Освещение по Фонгу
Здесь принято делать, как минимум, такие предположения:
- Ks = 0 (то есть грань не отражает свет, а только рассеивает)
Да-да, здесь нет никакой ошибки. Практически все обычно используемые (в demo
по меньшей мере) методы т.н. "освещения по Фонгу" НЕ учитывают отраженной
компоненты освещенности.
Здесь будет рассказано о самом, наверное, популярном методе освещения по
Фонгу, который сводит освещение к текстурированию по определенной текстуре.
Этот метод базируется на таких добавочных предположениях:
- L - константа (как бы точечный источник, удаленный бесконечно далеко)
- длина единичной нормали к объекту при интерполяции между вершинами
грани НЕ меняется
То есть, утверждается, что если в вершинах нормаль к объекту имеет длину 1,
то при интерполяции этой нормали между вершинами по какой-то грани мы будем
получать в каждой точке нормаль той же самой длины 1. На самом деле это вовсе
даже не так, но для несильно разнящихся углов наклона нормалей приблизительно
верно.
Так как Ks = 0, а длина N по предположению равна 1 на всей грани, имеем:
intensity = ambient + amp * (N * L).
Рассмотрим упрощенный случай, когда вектор L = (0,0,1). Общий случай можно
без особых вычислительных затрат привести к этому упрощенному, как - будет
рассказано чуть позже. Так вот, в этом случае
intensity = ambient + amp * (N.x * L.x + N.y * L.y + N.z * L.z) =
= ambient + amp * (N.x * 0 + N.y * 0 + N.z * 1) =
= ambient + amp * N.z =
= ambient + amp * sqrt(1 - (N.x * N.x + N.y * N.y)).
То есть интенсивность выражается через N.x, N.y, а эти величины меняются
линейно. N.x и N.y у нас - числа с плавающей запятой от -1 до 1 (т.к. длина
вектора равна 1), интерполировать их - занятие медленное, да корень считать
раз в пиксел тоже не хочется. Поэтому вместо интерполяции N.x и N.y обычно
интерполируют, например, 128*(N.x+1) и 128*(N.y+1), причем уже в целых
числах. Тогда все возможные значения таким образом отмасштабировнных N.x,
N.y - это 0, 1, ..., 255. Поэтому можно заранее посчитать табличку значений
intensity для каждой пары отмасштабировнных N.x, N.y.
То есть, мы линейно интерполируем 128*(N.x+1) и 128*(N.y+1) (эти значения
меняются тоже линейно, раз N.x, N.y меняются линейно) и по ним по таблице
определяем интенсивность. Это и есть текстурирование, только в качестве
текстуры используется таблица освещенности размером 256x256 (или любым
другим), а в качестве координат текстуры u, v для каждой вершины берутся
отмасшатбированные координаты нормали в этой вершине.
Таблица, согласно всего вышеупомянутого, считается так:
// ...
for (i = 0; i < 256; i++) {
for (j = 0; j < 256; j++) {
r =
pow((i - 128) / 256.0, 2) + // это N.x*N.x
pow((j - 128) / 256.0, 2); // это N.y*N.y
// длина N меньше 1, поэтому r > 1 быть не может
if (r > 1) r = 1;
phongTable[i][j] = amp * sqrt(1 - r);
}
}
// ...
Правда, обычно используют другую - нелинейную - таблицу, видимо, хоть для
какой-то компенсации всяких ошибок линеаризации... Результаты выглядят
действительно получше. Считается она так:
// ...
for (i = 0; i < 256; i++)
for (j = 0; j < 256; j++)
phongTable[i][j] =
amp * pow(sin(i * PI / 256) * sin(j * PI / 256), 4);
// ...
Для полного комплекта осталось только привести кусочек кода по вычислению
координат в этой таблице:
// ...
len = N.x * N.x + N.y * N.y + N.z * N.z;
N.x /= len; // на случай, если длина N не равна 1
N.y /= len;
N.z /= len;
u = (1 + N.x) * 128; // собственно расчет координат
v = (1 + N.y) * 128;
// ...
Теперь вернемся к вопросу о том, как привести случай с произвольным вектором
освещения к только что рассмотренному, где L = (0,0,1). Здесь все вроде бы
просто. Просто применим к нормалям в вершинах любой поворот, совмещающий наш
произвольный вектор света с вектором (0,0,1). Скалярное произведение при этом
не изменяется, поэтому так делать можно. Ну, а после этого поворота уже имеем
только что расписанный упрощенный случай.
Этот поворот нормалей в каждой вершине не требует практически никаких затрат
по следующей причине. Поворот сам по себе, конечно, достаточно медленная
процедура и процессорное время отъедает. Но при движении и вращении камеры
и объекта мы все равно должны будем соответственно поворачивать нормали. Так
вот, эти два поворота можно совместить в один. Если использовать матрицы,
все это делается совсем просто - достаточно перемножить (в нужном порядке!)
матрицу собственного поворота объекта, матрицу перехода от произвольной
камеры к нашей "стандартной" камере и матрицу перехода от произвольного
вектора света к "стандартному" вектору света (0,0,1). Т.е. добавится расчет
этой матрицы перехода и одно матричное умножение на объект, а это уже мелочь.