Модель освещения
Освещенность произвольно взятой точки P, появившуюся из-за источника света, излучающего во все стороны (omnilight) в общем случае будем вычислять по уравнению Фонга:
ambient = Ka, diffuse = Kd * cos(N, L), specular = Ks * pow(cos(R, V), Ns), intensity = ambient + amp * (diffuse + specular).
Здесь использованы следующие обозначения:
Ka | коэффициент фоновой интенсивности (характеристика окружающей среды) | Kd | коэффициент рассеяния (характеристика поверхности) | Ks | коэффициент отражения (характеристика поверхности) | Ns | коэффициент вида отражения (характеристика поверхности) | amp | "мощность" источника света | P | рассматриваемая точка | N | нормаль к поверхности изображаемого объекта в точке P | L | вектор, проведенный из точки P в источника света (луч света) | V | вектор, проведенный из точки P в "точку зрения" камеры | R | отраженный луч света (отражение L относительно N) | ambient | "фоновая" освещенность | diffuse | "рассеянная" освещенность | specular | "отраженная" освещенность | intensity | освещенность (суммарная) | cos(A,B) | косинус угла между векторами A и B |
Это уравнение даже поддается относительно простому объяснению - освещенность как бы складывается из фонового уровня освещенности, рассеянного (по всем направлениям - а значит, и по направлению глаза) в этой точке света от источника и отраженного (тоже в глаз) света от источника.
Как обычно, расчеты по этой формуле дадут довольно реалистичный результат, но считать все это для каждой точки грани слишком медленно.
Да, косинус угла между векторами считается как
cos(A,B) = A*B/(|A|*|B|),
где A*B - скалярное произведение векторов, |A| и |B| - их длины. Поэтому имеет смысл все векторы перед использованием привести к длине 1, тогда косинус угла между векторами будет равен их скалярному произведению. Небольшая оптимизация.
Расчет нормали к объекту
Во всех формулах для освещенности у нас так или иначе будет фигурировать вектор N - нормаль к объекту в точке P. Сразу возникает вопрос, а как же этот вектор считать.
Обычно придерживаются такой логики. Модель у нас состоит из плоских граней, но эта сетка плоских граней приближает какой-то искривленный объект. Нормаль к этому искривленному объекту меняется в каждой точке, а для плоских граней она постоянна для всех точек грани, и резко меняется при переходе на другую грань. Поэтому нормаль к объекту обычно приближают следующим образом: считают нормали в вершинах, а нормаль в какой-то точке грани линейно интерполируют между вершинами; то есть линейно интерполируют по грани все три координаты нормали.
Нормаль в вершине рассчитываются как сумма приведенных к длине 1 нормалей ко всем граням, к которым принадлежит эта вершина. То есть. Сначала считаем нормали ко всем граням и приводим их к длине 1. Далее, для каждой вершины надо перебрать все грани, и если очередная грань содержит эту вершину, то к нормали в вершине прибавляется нормаль к этой грани. Первоначально все нормали к вершинам полагаются равными нулю. Для больших моделей этот процесс может быть довольно долгим, но достаточно провести его заранее один раз и сохранить все посчитанные нормали к вершинам.
Для вящей понятности приведу кусок кода:
// ... for (i = 0; i < numberOfVertics; i++) { vertexNormal[i].x = 0; vertexNormal[i].y = 0; vertexNormal[i].z = 0; } for (i = 0; i < numberOfVertics; i++) { for (j = 0; j < numberOfFaces; j++) { if (face[j].vertex0 == i || face[j].vertex1 == i || face[j].vertex2 == i) { vertexNormal[i].x += faceNormal[j].x; vertexNormal[i].y += faceNormal[j].y; vertexNormal[i].z += faceNormal[j].z; } } } // ...
Но это метод даже слишком лобовой, и поэтому медленный. Можно сделать все проще и быстрее: перебираем все грани, и к нормалям всех принадлежащих грани вершин добавляем нормаль грани. После этого приводим все нормали к длине 1, причем эта фаза даже не обязательна, а лишь удобна для дальнейших расчетов. Соответствующий кусочек кода:
// ... for (i = 0; i < numberOfVertics; i++) { vertexNormal[i].x = 0; vertexNormal[i].y = 0; vertexNormal[i].z = 0; } for (i = 0; i < numberOfFaces; i++) { vertexNormal[face[i].vertex0].x += faceNormal[j].x; vertexNormal[face[i].vertex1].y += faceNormal[j].y; vertexNormal[face[i].vertex2].z += faceNormal[j].z; } // ...
1 2 3 4
8 8 8
| |