Урок 8. Освещение

В этом уроке мы будем учиться освещать и затенять наши модели. Вот список того, что мы изучим:
  • Как сделать так, чтобы объект был ярче когда находится ближе к источнику света.
  • Как сделать отблески когда мы видим отраженный свет на предмете(specular lighting)
  • Как сделать, чтобы объект был немного затененный, когда свет падает не прямо на объект(diffuse lighting)
  • Подсветка сцены(ambient lighting)

В этом уроке мы не будем рассматривать.
  • Тени. Эта тема заслуживает отдельного урока(или уроков, если даже не книг).
  • Зеркальное отражение(например, вода)
  • Подповерхностное рассеивание(например, как у воска)
  • Анизотропные материалы(окрашенный металл, например)
  • Затенение основанное на физических процессах, чтобы имитировать реальность еще лучше.
  • Преграждение света(Ambient Occlusion если что-то преграждает свет, то становится темнее)
  • Отражение цвета(красный ковер будет делать белый потолок слегка слегка красноватым)
  • Прозрачность
  • Глобальное освещение(в принципе все что мы указали выше можно назвать этим термином)

Другими словами, самое простое освещение и затенение.

Нормали

В прошлом уроке мы работали с нормалями, но без особого понимания, зачем они вообще нужны.

Нормали Треугольников

Нормаль к плоскости — это единичный вектор который направлен перпендикулярно к этой плоскости.
Нормаль к треугольнику — это единичный вектор направленный перпендикулярно к треугольнику. Нормаль очень просто рассчитывается с помощью векторного произведения двух сторон треугольника(если вы помните, векторное произведение двух векторов дает нам перпендикулярный вектор к обоим) и нормализованный: его длина устанавливается в единицу.
Вот псевдокод вычисления нормали:

треугольник( v1, v2, v3 )
сторона1 = v2-v1
сторона2 = v3-v1
треугольник.нормаль = вектПроизведение(сторона1, сторона2).нормализировать()

Вершинная Нормаль

Это нормаль введенная для удобства вычислений. Это комбинированная нормаль от нормалей окружающих данную вершину треугольников. Это очень удобно, так как в вершинных шейдерах мы имеем дело с вершинами, а не с треугольниками. В любом случае в OpenGL у мы почти никогда и не имеем дела с треугольниками.

вершина v1, v2, v3, ....
треугольник tr1, tr2, tr3 // они все используют вершину v1
v1.нормаль = нормализовать( tr1.нормаль + tr2.нормаль + tr3.нормаль)


Использование нормалей вершин в OpenGL

Использовать нормали в OpenGL очень просто. Нормаль — это просто атрибут вершины, точно так же, как и позиция, цвет или UV координаты...Тоесть ничего нового учить не придется...даже наша простенькая функция loadOBJ уже загружает нормали.

GLuint normalbuffer;
glGenBuffers(1, &normalbuffer);
glBindBuffer(GL_ARRAY_BUFFER, normalbuffer);
glBufferData(GL_ARRAY_BUFFER, normals.size() * sizeof(glm::vec3), &normals[0], GL_STATIC_DRAW);

и

// Третий атрибутный буфер : нормали
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, normalbuffer);
glVertexAttribPointer(
   2,                                // атрибут
   3,                                // размер
   GL_FLOAT,                        // тип
   GL_FALSE,                        // нормализованный ли?
   0,                                // шаг
   (void*)0                         // смещение в буфере
);

И этого достаточно чтобы начать:

Диффузное освещение

Важность нормали к поверхности

Когда световой луч попадает на поверхность, большая его часть отражается во все стороны. Это называется «диффузная компонента». Остальные компоненты мы рассмотрим чуть позже.


После падения луча, поверхность отражает свет по разному, в зависимости от угла под которым падает этот луч к поверхности. Если луч падает перпендикулярно к поверхности, то он концентрируется на маленьком участке, если по касательной, то рассеивается на гораздо большей поверхности:

Это значит, что под непрямым светом поверхность будет слегка темнее(и хотя общее количество света будет тем же, просто он рассеивается на большей поверхности)
С точки зрения компьютерной графики, цвет пикселя очень зависит от разности углов направления света и нормали поверхности.

// Косинус угла между нормалью и направлением света
// 1 — если свет перпендикулярен к треугольнику
// 0 — если свет параллелен к треугольнику
float cosTheta = dot( n,l );
color = LightColor * cosTheta;

В этом коде «n» - это нормаль, а «l» - единичный вектор который идет от поверхности к источнику света(а не наоборот, хотя это может показаться  непонятным)

Будьте внимательны со знаком

Иногда наша формула будет не работать. Например, когда свет будет находиться за треугольником, n и l будут противоположны, поэтому n.l будет отрицательным. И в итоге у нас будет какой-то отрицательный цвет, и в итоге какой-то бред. Поэтому мы приведем все отрицательный числа к 0 с помощью функции clamp.

// Косинус угла между нормалью и направлением света
// 1 — если свет перпендикулярен к треугольнику
// 0 — если свет параллелен к треугольнику
// 0 — если свет позади треугольника
float cosTheta = clamp( dot( n,l ), 0,1 );
color = LightColor * cosTheta;


Цвет материала

Конечно цвет предмета должен очень сильно зависеть от цвета материала. Белый свет состоит из трех компонент — красного, синего и зеленого. Когда свет падает на красную поверхность, то зеленая и синяя компоненты поглощаются, а красная отражается.


Мы можем промоделировать это простым умножением:

color = MaterialDiffuseColor * LightColor * cosTheta;

Моделирование света

Давайте предположим, что у нас есть точечный источник света, который излучает свет во все направления, как, например, свечка.
С таким источником света, уровень освещения поверхности будет зависеть от расстояния до источника света: чем дальше, тем темнее. Эта зависимости рассчитывается так:

color = MaterialDiffuseColor * LightColor * cosTheta / (distance*distance);

Вскоре нам понадобится еще один параметр чтобы управлять уровнем силы света — цвет света, но пока, давайте предположим, что у нас есть лампочка белого света с определенной мощностью(например, 60 ватт).

color = MaterialDiffuseColor * LightColor * LightPower * cosTheta / (distance*distance);

Объединяем все вместе

Чтобы этот код работал нам нужен определенный набор параметров(цвета и мощности) и немного дополнительного кода.

MaterialDiffuseColor — мы можем взять прямо из текстуры.
LightColor и LightPower нужно будет выставить в шейдере с помощью GLSL uniform.
CosTheta будет зависеть от векторов n и l. Его можно вычислять для любого из пространств, угол будет одним и тем же. Мы будем использовать пространство камеры, так как тут очень просто посчитать положение светового источника:

// Нормаль фрагмента в пространстве камеры
vec3 n = normalize( Normal_cameraspace );
// Направление света(от фрагмента к источнику света
vec3 l = normalize( LightDirection_cameraspace );

Normal_cameraspace и LightDirection_cameraspace подсчитываются в вершинном шейдере и передаются во фрагментный для дальнейшей обработки:

// Позиция вершины в пространстве камеры :МВП * положение
gl_Position =  MVP * vec4(vertexPosition_modelspace,1);
// Положение вершины в мировом пространстве: M * положение
Position_worldspace = (M * vec4(vertexPosition_modelspace,1)).xyz;
// Вектор который идет от вершины  камере в пространстве камеры
// В пространстве камеры, камера находится по положению (0,0,0)
vec3 vertexPosition_cameraspace = ( V * M * vec4(vertexPosition_modelspace,1)).xyz;
EyeDirection_cameraspace = vec3(0,0,0) - vertexPosition_cameraspace;
// Вектор который идет от вершины к источнику света в пространстве камеры.
//Матрица M пропущена, так как она в в этом пространстве единичная.
vec3 LightPosition_cameraspace = ( V * vec4(LightPosition_worldspace,1)).xyz;
LightDirection_cameraspace = LightPosition_cameraspace +
EyeDirection_cameraspace;
// Нормаль вершины в пространстве камеры
Normal_cameraspace = ( V * M * vec4(vertexNormal_modelspace,0)).xyz; // Будет работать лишь в том случае, когда матрица модели не изменяет её размер.

На первый взгляд код может показаться довольно сложным и запутанным, но на самом деле, тут нет ничего нового чего не было в уроке 3: Матрицы. Я старался давать каждой переменной осмысленные имена, чтобы вам было легко понять что и как тут происходит.
Обязательно попробуйте!!!

M и V – это матрицы Модели и Вида, которые передаются в шейдер точно так же, как и наша старая добрая MVP.

Время испытаний

Я рассказал вам все что нужно, чтобы сделать диффузное освещение. Вперед, попробуйте.

Результат

Только лишь с одной диффузной компонентой у нас получается вот такая вот картинка(простите меня за некрасивые текстуры).


Вроде бы как получше, чем было раньше, но многого еще не хватает. Особенно заметна проблема с неосвещенными частями. Затылок нашей дорогой мартышки Сюзанны полностью черный(мы ведь использовали clamp()).

Окружающее освещение(ambient lighting)

Окружающее освещение – это чистой воды читерство.
Затылок Сюзанны не должен быть полностью черным, так как в реальной жизни свет от лампы должен упасть на стену, пол, потолок, частично отразиться от него, и осветить теневую часть объекта.
Однако это слишком вычислительно затратно делать в реальном времени. И именно поэтому мы будем добавлять некую постоянную составляющую. Как будто сам объект излучает немного света, чтобы не быть полностью черным.

vec3 MaterialAmbientColor = vec3(0.1,0.1,0.1) * MaterialDiffuseColor;
color =
// Окружающее освещение :симулируем непрямое освещение
MaterialAmbientColor +
// Диффузное : "цвет" самого объекта
MaterialDiffuseColor * LightColor * LightPower * cosTheta /
(distance*distance);


Результат

Вот так вот будет немного лучше. Вы можете по играться  с коефициентами (0.1, 0.1, 0.1) чтобы попробовать добиться лучшего результата.



Отраженный свет(Specular light)

Часть света которая отражается, в основном отражается в сторону отраженного луча к поверхности.


Как мы видим на рисунке, отраженный свет формирует световое пятно. В некоторых случаях, когда диффузная компонента равна нулю, это световое пятно очень очень узкое(весь свет полностью отражается в одном направлении) и мы получаем зеркало.
(однако, хотя вы можете подправить параметры чтобы получить зеркало, в нашем случае оно будет принимать во внимание лишь отражение нашего источника света. Так что получится странное зеркало)


// вектор взгляда(в сторону камеры)
vec3 E = normalize(EyeDirection_cameraspace);
//Направление в котором треугольник отражает свет
vec3 R = reflect(-l,n);
// Косинус угла между вектором взгляда и вектором отражения обрезанный до нуля если нужно
//  - Смотрим прям на отражение -> 1
//  -Смотрим куда-то в другую сторону -> < 1
float cosAlpha = clamp( dot( E,R ), 0,1 );
color =
   // Окружающее освещение :симулируем непрямое освещение
   MaterialAmbientColor +
   // Диффузное : "цвет" самого объекта
   MaterialDiffuseColor * LightColor * LightPower * cosTheta /
(distance*distance) ;
   // Отраженное : отраженные отблески, как зеркало
   MaterialSpecularColor * LightColor * LightPower * pow(cosAlpha,5) / (distance*distance);

R – направление  в которое отражается свет. E – инвертированный вектор направления взгляда. Если угол между этими векторами маленький, мы смотрим прямо на отражение.
pow(cosAlpha,5) используется для того, чтобы контролировать размер светового пятна. Увеличивайте 5, чтобы получить пятнышко меньше.

Финальный результат



Обратите внимание на отблески на носу, глазах и бровях.
Этот метод затенения использовался на протяжении многих лет, так как он очень простой. Однако он обладает множеством недостатков, поэтому сейчас стали применять другие методы затенения: например микрофасеточный BRDF, но об этом позже.

В следующем уроке мы будем разбирать, как можно ускорить рендеринг нашего VBO.

3 комментария:

  1. Здравствуйте, уже несколько дней туплю, помогите разобраться) Почему в вертоксном шейдере мы обрабатываем нормали вершин(другие он и не может, это понятно), потом передаем их фрагментному шейдеру, а там обращаясь к ним "Normal_cameraspace" - уже как нормаль фрагментов обрабатываем??

    И еще тут:
    Normal_cameraspace = ( V * M * vec4(vertexNormal_modelspace,0)).xyz; // Будет работать лишь в том случае, когда матрица модели не изменяет её размер.

    не будет работать потому что они не будут нормироваными или что?

    ОтветитьУдалить
  2. почему cosTheta вычисляется так
    float cosTheta = dot( n,l );

    формула такая
    dot(v1,v2) == length(v1)*length(v2)*cos(angle)
    dot(v1,v2)/(length(v1)*length(v2)) == cos(angle)

    должно быть loat cosTheta = dot( n,l )/((length(n) * length(l))

    ОтветитьУдалить
    Ответы
    1. Vladimir, n и l единичные, length(n) * length(l) = 1, смысл на единицу делить?

      Удалить