в самое начало


demo.design
3D programming FAQ



ОСВЕЩЕНИЕ
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). Т.е. добавится расчет этой матрицы перехода и одно матричное умножение на объект, а это уже мелочь.



 в самое начало


demo.design
3D programming FAQ