Биографии Характеристики Анализ

Включение источников света в opengl. Продолжаем изучение OpenGL: освещение по Фонгу

Часть 1. Подготовка

Для изучения освещения необходимы:

  • OpenGL ;
  • glut ;
  • IDE , хотя можно воспользоваться gedit или CodeBlocks ;
  • компилятор, например, gcc для Linux и mingw для Windows;
Часть 2. Пример простой программы

Рассмотрим пример программы, в которой используется освещение.

Код:

/*http://сайт, isaer*/ #include #include #include void init() { glClearColor(0.3, 0.3, 0.3, 1.0); glEnable(GL_LIGHTING); glLightModelf(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE); glEnable(GL_NORMALIZE); } void reshape(int width, int height) { glViewport(0, 0, width, height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(-1.2, 1.2, -1.2, 1.2, -1, 1); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } void init_l() { float light0_diffuse = {0.4, 0.7, 0.2}; float light0_direction = {0.0, 0.0, 1.0, 0.0}; glEnable(GL_LIGHT0); glLightfv(GL_LIGHT0, GL_DIFFUSE, light0_diffuse); glLightfv(GL_LIGHT0, GL_POSITION, light0_direction); } void display() { glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); init_l(); GLfloat x, y; glBegin(GL_QUADS); glNormal3f(0.0, 0.0, -1.0); for (x = -1.0; x < 1.0; x += 0.005) { for (y = -1.0; y < 1.0; y += 0.005) { glVertex3f(x, y, 0.0); glVertex3f(x, y + 0.005, 0.0); glVertex3f(x + 0.005, y + 0.005, 0.0); glVertex3f(x + 0.005, y, 0.0); } } glEnd(); glDisable(GL_LIGHT0); glutSwapBuffers(); } int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH); glutInitWindowPosition(50, 100); glutInitWindowSize(500, 500); glutCreateWindow("Light"); init(); glutDisplayFunc(display); glutReshapeFunc(reshape); glutMainLoop(); }

Часть 3. Разбор кода в примере

Комментарии даны через символ // - "слэш".

Код:


Здесь инициализируется освещение

Код:


Здесь происходит вся прорисовка.

Код:


Часть 4. Изучаем освещение

Сначала включим расчет освещения командой glEnable(GL_LIGHTING) . Затем надо разблокировать источник света командой glEnable(GL_LIGHT) . GL_LIGHT может принимать только 8 значений (по крайней мере в OpenGL 2.1), то есть GL_LIGHT0..GL_LIGHT7 .

Теперь надо создать источник света. У каждого источника света есть свои параметры по умолчанию, например, если вы просто разблокируете 2 источника света GL_LIGHT0 и GL_LIGHT1, то будет виден только 0, так как в нем параметры по умолчанию отличаються от остальных (у всех остальных они идентичны).

Источники света имеют несколько параметров, а именно: цвет, позиция и направление.

Команда, используемая для указания всех параметров света - это glLight*() . Она принимает три аргумента: идентификатор источника света, имя свойства и желаемое для него значение.

Код:


Если нет, то тогда это единственное значение.

Например:

Код:

/*http://сайт, isaer*/ glLightf(GL_LIGHT0, GL_GL_SPOT_CUTOFF, 180);

Вот листинг значений GLenum pname .

Читается так: первая строчка - это название параметра, вторая - это значение поумолчанию, а третья - пояснение. Если вы видите что-то типа (1.0,1.0,1.0,1.0) или (0.0,0.0,0.0,1.0), то это значит, что первая скобка - это значение по умолчанию для нулевого источника, а вторая скобка - для остальных:

Код:


Пример использования освещения:

Код:

/*http://сайт, isaer*/ float light_ambient = {0.0,0.0,0.0,1.0}; float light_diffuse = {1.0,1.0,1.0,1.0}; float light_specular = {1.0,1.0,1.0,1.0}; float light_position = {1.0,1.0,1.0,0.0}; glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient); glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse); glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular); glLightfv(GL_LIGHT0, GL_POSITION, light_position);

Часть 5. Изучаем параметры света

1. Цвет

Diffuse

Параметр GL_DIFFUSE, наверное, наиболее точно совпадает с тем, что вы привыкли называть «цветом света». Он определяет RGBA-цвет диффузного света, который отдельный источник света добавляет к сцене.

Ambient

Параметр GL_AMBIENT влияет на цвет зеркального блика на объекте. В реальном мире на объектах вроде стеклянной бутылки имеется зеркальный блик соответствующего освещению цвета (часто белого).

Specular

Параметр GL_SPECULAR влияет на интенсивность зеркального блика на объектах.

2. Позиция

Параметр GL_POSITION (x, y, z, w) имеет 3 значения положения и одно, указывающее на то, какой источник света будет использоваться.

Первые 3 (x, y, z) параметра понятны, а 4-й (w) параметр указывает, будет ли использоваться бесконечно удаленный свет или точечный. Если значение w = 0 , то источник света бесконечно удаленный (что-то вроде солнца). Если w = 1 , то этот источник света точечный (что-то вроде лампочки).

Если w = 0 , то первые 3 параметра - это вектор от центра системы координат (0,0,0).

3. Прожектор

GL_SPOT_DIRECTION - направление света прожектора.

GL_SPOT_EXPONENT - концентрация светового луча.

GL_SPOT_CUTOFF - угловая ширина светового луча.

Единственное, что нужно уточнить - позиция должна быть w = 1 .

4. Ослабление

Если вам нужно ослаблять интенсивность света от центра (то есть чем дальше от центра, тем тускнее), то вам надо настроить параметры: GL_CONSTANT_ATTENUATION, GL_LINEAR_ATTENUATION, GL_QUADRATIC_ATTENUATION .

Но так не очень удобно, поэтому можно воспользоваться формулой:

Код:


Радиус задается от центра до конца.

Если вам надо уменьшить общую интенсивность, вы можете изменить параметр att .

Часть 6. Выбор модели освещения

OpenGL-понятие модели освещения разделяется на 4 компонента:

  • интенсивность глобального фонового света;
  • считается ли положение точки наблюдения локальным к сцене или бесконечно удаленным;
  • должен ли расчет освещенности производиться по-разному для лицевых и обратных граней объектов;
  • должен ли зеркальный цвет отделяться от фонового и диффузного и накладываться на объект после операций текстурирования.
glLightModel*() – это команда, используемая для задания всех параметров модели освещения. Может принимать два аргумента: имя параметра модели освещения в виде константы и значение для этого параметра.

Код:


Теперь вы можете создавать отличные источники света.

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

  • Каксделатьтак,чтобыобъектбылярчекогданаходитсяближекисточникусвета.
  • Каксделатьотблескикогдамывидимотраженныйсветнапредмете(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);

glBufferData(GL_ARRAY_BUFFER, normals.size() * sizeof(glm::vec3), &normals, GL_STATIC_DRAW);

// Третий атрибутный буфер: нормали
glEnableVertexAttribArray(2);

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

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


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

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

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

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


С точки зрения компьютерной графики, цвет пикселя очень зависит от разности углов направления света и нормали поверхности.


//
//
float cosTheta = dot(n,l);

В этом коде «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)
vec 3 vertexPosition _ cameraspace = ( V * M * vec 4( 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);
//Направление в котором треугольник отражает свет
vec 3 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) /

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

Чтож, господа. За последнее время мы довольно много узнали об OpenGL, в том числе научились управлять камерой , работать с текстурами , а также с моделями . Настало время поговорить о чем-то намного более интересном, а именно — об освещении. Интересна эта тема, потому что ничего готового для работы со светом в OpenGL нет, все нужно писать самостоятельно на шейдерах. В рамках этой заметки мы рассмотрим освещение по Фонгу. Это довольно большая тема, поэтому говорить мы будем исключительно об освещении . В том, как делаются тени , придется разобраться в другой раз.

Сохранение и использование нормалей

Прежде, чем перейти непосредственно к освещению, нам понадобится такая штука, как нормали.

Мы уже знаем, что у моделей есть вершины и соответствующие этим вершинам UV-координаты. Для создания освещения нам понадобится еще кое-какая информация о моделях, а именно — нормали. Нормаль — это единичный вектор, соответствующей вершине (или, как вариант, полигону, но это не наш случай). Какую именно роль играют нормали при реализации освещения мы узнаем ниже. Пока достаточно сказать, что нормали действительно очень важны. Например, благодаря им поверхности выглядят более гладкими и можно отличить шар от правильного выпуклого многогранника вроде икосаэдра . А раз нормали так важны, нам нужно научиться их сохранять при преобразовании моделей Blender в наш собственный формат.

Соответствующие изменения довольно тривиальны. Мы получаем нормали точно так же, как получали координаты вершин и UV-координаты:

// часть тела процедуры importedModelCreate

for (unsigned int j = 0 ; j < face.mNumIndices ; ++ j) {
unsigned int index = face.mIndices [ j] ;
aiVector3D pos = mesh- > mVertices[ index] ;
aiVector3D uv = mesh- > mTextureCoords[ 0 ] [ index] ;
aiVector3D normal = mesh- > mNormals[ index] ;

VerticesBuffer[ verticesBufferIndex++ ] = pos.x ;
verticesBuffer[ verticesBufferIndex++ ] = pos.y ;
verticesBuffer[ verticesBufferIndex++ ] = pos.z ;
verticesBuffer[ verticesBufferIndex++ ] = normal.x ;
verticesBuffer[ verticesBufferIndex++ ] = normal.y ;
verticesBuffer[ verticesBufferIndex++ ] = normal.z ;
verticesBuffer[ verticesBufferIndex++ ] = uv.x ;
verticesBuffer[ verticesBufferIndex++ ] = 1.0f - uv.y ;
}

Аналогично изменяется процедура оптимизации модели. А в процедуре modelLoad вместо двух массивов атрибутов нам теперь потребуется три:

// часть тела процедуры modelLoad

GlBindVertexArray(modelVAO) ;
glEnableVertexAttribArray(0 ) ;
glEnableVertexAttribArray(1 ) ;
glEnableVertexAttribArray(2 ) ;

GlBindBuffer(GL_ARRAY_BUFFER, modelVBO) ;
glBufferData(GL_ARRAY_BUFFER, header- > verticesDataSize, verticesPtr,
GL_STATIC_DRAW) ;

GLsizei stride = 8 * sizeof (GLfloat) ;
glVertexAttribPointer(0 , 3 , GL_FLOAT, GL_FALSE, stride, nullptr) ;
glVertexAttribPointer(1 , 3 , GL_FLOAT, GL_FALSE, stride,
(const void * ) (3 * sizeof (GLfloat) ) ) ;
glVertexAttribPointer(2 , 2 , GL_FLOAT, GL_FALSE, stride,
(const void * ) (6 * sizeof (GLfloat) ) ) ;

Также нам дополнительно понадобится uniform-переменная с матрицей M:

GLint uniformM = getUniformLocation(programId, "M" ) ;

// ...

GlUniformMatrix4fv(uniformM, 1 , GL_FALSE, & towerM[ 0 ] [ 0 ] ) ;

… чтобы в vertex shader правильно повернуть нормаль в пространстве:

#version 330 core

Layout(location = 0 ) in vec3 vertexPos;
layout(location = 1 ) in vec3 vertexNorm;
layout(location = 2 ) in vec2 vertexUV;

uniform mat4 MVP;
uniform mat4 M;

out vec2 fragmentUV;
out vec3 fragmentNormal;
out vec3 fragmentPos;

void main() {
fragmentUV = vertexUV;
fragmentNormal = (M * vec4 (vertexNorm, 0 ) ) . xyz ;
fragmentPos = (M * vec4 (vertexPos, 1 ) ) . xyz ;

gl_Position = MVP * vec4 (vertexPos, 1 ) ;
}

Наконец, fragment shader принимает интерполированную по трем вершинам нормаль:

// ...

void main() {

// ...
}

Таким вот незамысловатым образом мы получаем нормали для фрагментов .

Что такое освещение по Фонгу

Как было отмечено, освещение в OpenGL пишется на шейдерах самим программистом. Понятно, что есть больше одного способа реализовать это освещение, каждый со своей степенью реалистичности и требовательностью к ресурсам. А у каждого способа еще может быть бесчисленное количество конкретных реализаций. Насколько я понимаю, эффективное и реалистичное освещение в реальном времени все еще является областью активных исследований. В рамках этой заметки мы рассмотрим освещение по Фонгу, которое одновременно является довольно реалистичным и простым в реализации.

Важно понимать разницу между следующими понятиями:

  • Затенение по Гуро (Gouraud shading) — это когда вы вычисляете освещенность каждой вершины, а освещенность фрагментов между ними интерполируется;
  • Затенение по Фонгу (Phong shading) — когда освещенность вычисляется отдельно для каждого фрагмента;
  • Освещение по Фонгу (Phong lighting или Phong reflection model) — конкретный способ освещения, о котором идет речь в этой заметке и который можно использовать как в затенении по Гуро, так и в затенении по Фонгу;

Не удивительно, что Phong shading и Phong lighting часто путают, и в некоторых туториалах можно прочитать ерунду вроде «Идея освещения Фонга (Phong shading) заключается в использовании трех компонентов …» что сразу заставляет сильно усомниться в авторитете написавшего этот туториал человека.

Насколько я смог понять, в современных приложениях затенение по Гуро почти не используется, вместо него предпочтение отдается затенению по Фонгу. В рамках данного поста мы тоже будем использовать затенение по Фонгу, то есть, освещение будет вычислять отдельно для каждого фрагмента. Конкретный способ освещения, которым мы воспользуемся — освещение по Фонгу. Этот способ заключается в следующем.

По разным формулам вычисляется три компонента освещения:

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

Затем результаты суммируются, в результате чего получается общее освещение.

Чтобы стало еще интереснее, источники света бывают разные. Очевидно, что солнце на улице и фонарик в темноте освещают сцену совсем по-разному. Для начала мы рассмотрим наиболее простой источник — направленный свет.

Направленный свет (directional light)

Направленный свет — это имитация бесконечно удаленного источника света. Возьмем, например, Солнце. Солнце находится очень далеко от Земли. Поэтому у поверхности Земли можно с большой точностью считать все лучи света от Солнца параллельным. Направленный свет характеризует его направление, цвет, а также кое-какие коэффициенты, которые понадобятся нам ниже:

struct DirectionalLight {
vec3 direction;

vec3 color;
float ambientIntensity;
float diffuseIntensity;
float specularIntensity;
} ;

В коде fragment shader определим процедуру calcDirectionalLight, которая будет использоваться как-то так:

in vec3 fragmentPos;
uniform vec3 cameraPos;
uniform DirectionalLight directionalLight;

// ...

void main() {
// normal should be corrected after interpolation
vec3 normal = normalize (fragmentNormal) ;


directionalLight) ;

// ...
}

Рассмотрим реализацию процедуры.

vec4 calcDirectionalLight(vec3 normal, vec3 fragmentToCamera,
DirectionalLight light) {
vec4 ambientColor = vec4 (light. color , 1 ) * light. ambientIntensity ;

// ...
}

Сначала вычисляется первый компонент — фоновое освещение. Это просто цвет излучаемого света умноженный на интенсивность фонового освещения. Пока все просто.

// ...

float diffuseFactor = max (0.0 , dot (normal, - light. direction ) ) ;
vec4 diffuseColor = vec4 (light. color , 1 ) * light. diffuseIntensity
* diffuseFactor;

// ...

Рассеянное освещение. Переменная diffuseFactor представляет собой косинус угла между нормалью к фрагменту и вектором, направленным от фрагмента к источнику света. Если свет падает перпендикулярно поверхности, угол равен нулю. Косинус этого угла равен единице и освещенность максимальна (см статью на Wikipedia о Законе Ламберта). С увеличением угла косинус уменьшается и становится равным нулю, если свет идет параллельно поверхности. Если косинус отрицательный, значит источник света находится где-то за поверхностью и она не освещена, поэтому отрицательные значения мы обращаем в ноль при помощи max(0.0, ...) . Помимо угла, под которым падает свет, также учитывается интенсивность рассеянного освещения diffuseIntensity.

// ...
vec3 lightReflect = normalize (reflect (light. direction , normal) ) ;
float specularFactor = pow (
max (0.0 , dot (fragmentToCamera, lightReflect) ) ,
materialSpecularFactor
) ;
vec4 specularColor = light. specularIntensity * vec4 (light. color , 1 )
* materialSpecularIntensity * specularFactor;
// ...

Отраженное освещение. Переменная lightReflect — это единичный вектор, задающий направление отраженного света. Переменная specularFactor вычисляется похожим на diffuseFactor способом, только на этот раз учитывается косинус угла между направлением, в котором отразился свет, и направлением от фрагмента до камеры. Если этот угол равен нулю, значит отраженный свет летит прямо в камеру и блики на поверхности максимальны. Если угол велик, значит никаких бликов не должно быть видно. Здесь materialSpecularFactor является uniform переменной. Чем она больше, тем меньше по площади блики на поверхности объекта. Также используется переменная materialSpecularIntensity, определяющая яркость бликов. Заметьте, что все это — свойства материала, а не света. Например, метал отражает свет, и потому имеет блики. А дерево свет не отражает, и потом вы никогда не видите бликов на деревьях (конечно, если поверхность сухая, и так далее).

В приведенном коде у света есть свойство specularIntensity. Но его следует использовать только в отладочных целях, чтобы подчеркнуть блики от определенного источника света. В релизной версии кода этот коэффициент должен либо равняться единице, либо вовсе быть выкинутым из кода.

Наконец, три компонента складываются и возвращается результат:

// ...

return ambientColor + diffuseColor + specularColor;
}

Не так уж и сложно, правда?

Точечный источник света (point light)

Точечный источник света — это, к примеру, горящая лампочка. Свет от лампочки направлен во все стороны. Поэтому точечный источник света не характеризуется направлением света, но характеризуется положением источника в пространстве:

struct PointLight {
vec3 position;

vec3 color;
float ambientIntensity;
float diffuseIntensity;
float specularIntensity; // for debug purposes, should be set to 1.0
} ;

Освещенность от точечного источника света легко вычисляется через уже имеющуюся процедуру calcDirectionalLight:

vec4 calcPointLight(vec3 normal, vec3 fragmentToCamera,
PointLight light) {
vec3 lightDirection = normalize (fragmentPos - light. position ) ;
float distance = length (fragmentPos - light. position ) ;
float pointFactor = 1.0 / (1.0 + pow (distance , 2 ) ) ;

DirectionalLight tempDirectionalLight = DirectionalLight(
lightDirection,
light. color ,
light. ambientIntensity ,
light. diffuseIntensity ,
light. specularIntensity
) ;
return pointFactor * calcDirectionalLight(normal, fragmentToCamera,
tempDirectionalLight) ;
}

Имея координаты фрагмента и источника света, можно легко вычислить направление света к заданному фрагменту через разность векторов. Множитель pointFactor отражает факт затухания света с квадратом расстояния до его источника (в соответствии с формулой зависимости площади поверхности сферы от радиуса). При вычислении pointFactor в делителе дополнительно прибавляется единица, чтобы предотвратить возможность деления на ноль. После этого все вычисляется точно так же, как для направленного света.

Прожектор (spot light)

В качестве примера этого источника света можно привести фонарик. Он похож на точечный источник света, только дополнительно имеет направление и угол влияния (cutoff):

struct SpotLight {
vec3 direction;
vec3 position;
float cutoff;

vec3 color;
float ambientIntensity;
float diffuseIntensity;
float specularIntensity; // for debug purposes, should be set to 1.0
} ;

Соответствующая процедура:

vec4 calcSpotLight(vec3 normal, vec3 fragmentToCamera,
SpotLight light) {
vec3 spotLightDirection = normalize (fragmentPos - light. position ) ;
float spotAngleCos = dot (spotLightDirection, light. direction ) ;
float attenuation = (1.0 - 1.0 * (1.0 - spotAngleCos) /
(1.0 - light. cutoff ) ) ;
float spotFactor = float (spotAngleCos > light. cutoff ) * attenuation;

PointLight tempPointLight = PointLight(
light. position ,
light. color ,
light. ambientIntensity ,
light. diffuseIntensity ,
light. ambientIntensity
) ;
return spotFactor * calcPointLight(normal, fragmentToCamera,
tempPointLight) ;
}

Направление света вычисляется точно так же, как и для точечного источника. Затем вычисляется косинус угла между этим направлением и направлением, указанным в свойствах самого источника света. При помощи выражения float(spotAngleCos > light.cutoff) свет жестко обрезается до указанного угла. Множитель attenuation добавляет плавное затухание света по мере отдаления фрагментов от направления света, указанного в свойствах источника. После этого все вычисления сводятся к вычислениям для точечного источника света.

Гамма-коррекция

Целиком процедура main во fragment shader выглядит так:

void main() {
// normal should be corrected after interpolation
vec3 normal = normalize (fragmentNormal) ;
vec3 fragmentToCamera = normalize (cameraPos - fragmentPos) ;

vec4 directColor = calcDirectionalLight(normal, fragmentToCamera,
directionalLight) ;
vec4 pointColor = calcPointLight(normal, fragmentToCamera,
pointLight) ;
vec4 spotColor = calcSpotLight(normal, fragmentToCamera, spotLight) ;
vec4 linearColor = texture(textureSampler, fragmentUV) *
(vec4 (materialEmission, 1 ) + directColor +
pointColor + spotColor) ;

vec4 gamma = vec4 (vec3 (1.0 / 2.2 ) , 1 ) ;
color = pow (linearColor, gamma) ; // gamma-corrected color
}

На materialEmission не обращайте особого внимания. Это просто еще одно свойство материала, добавляющее ему самостоятельное свечение. Многие объекты светятся сами по себе. Взять те же лампочки, которые служат источником света для других объектов. Мы ведь должны видеть их в полной темноте, даже если лампочки не освещены никаким другим источником света, верно?

Что действительно заслуживает внимания — это гамма-коррекция , которая заключается в возведении всех компонентов света в степень 1/2.2. До сих пор мы работали в линейном пространстве цветов, исходя из предположения, что цвет с яркостью 1.0 в два раза ярче цвета с яркостью 0.5. Проблема в том, что человеческий глаз воспринимает яркость не линейно. Поэтому для получения реалистичного освещения необходимо после всех вычислений в линейном пространстве производить гамма-коррекцию.

Следует учитывать, что при сохранении изображения современные графические редакторы также выполняют гамма-коррекцию. Поэтому перед использованием текстур нужно эту гамма-коррекцию отменить. К счастью, это не сложно.

Достаточно заменить в коде загрузки текстур все константы:

GL_COMPRESSED_RGBA_S3TC_DXT1_EXT
GL_COMPRESSED_RGBA_S3TC_DXT3_EXT
GL_COMPRESSED_RGBA_S3TC_DXT5_EXT

GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT
GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT
GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT

… соответственно. Так мы сообщим, что к изображением была применена гамма-коррекция, которую нужно отменить. Об остальном OpenGL позаботится сам.

В реальных приложениях параметр gamma (у нас gamma = 2.2) лучше выносить в настройки программы, чтобы пользователь при желании мог немного подстроить его под свой монитор.

Заключение

Настало время разглядывать картинки!

Здесь мы видим различные компоненты освещения. Слева направо, сверху вниз: фоновое, рассеянное, отраженное, все три вместе. Как видите, на сцену была добавлена модель тора . Из-за сложного расположения нормалей эту модель рекомендуется использовать для тестирования освещения.

Различные источники света. Слева направо, сверху вниз: белый направленный свет, красный точечный источник света, синий прожектор, все три вместе.

Отмечу еще раз, что один и тот же метод освещения может иметь разные реализации. Например, можно сделать свойства материала ambient, diffuse и specular color, что позволит рисовать красные объекты, рассеивающие зеленый цвет и имеющие синие блики. В некоторых реализациях освещения по Фонгу я видел вычисление фонового освещения один раз, а не для каждого источника света. Также я видел реализации, где свет от точечного источника затухал не просто пропорционально квадрату расстояния до него (d * d), а по более общей формуле (в стиле A + B*d + C*d*d). Кто-то делает ambient intensity и diffuse intensity свойством не только источника света, но и материала. Не уверен, правда, насколько все это имеет отношение к реалистичности освещения. Но в качестве домашнего задания можете поиграться со всем этим.

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

Стоит отметить, что при использовании FFP OpenGL освещение рассчитывалось не в мировой системе координат, а в видовой. На мой взгляд это не очень удобно, т.к. видовая система координат связана с камерой, а источники освещения удобно задавать в мировой системе координат и именно там и производить весь расчет.

Расчет освещения

Для расчетов освещения в этом уроке используется модель освещения Фонга (Phong shading). Основной смысл модели в том, что итоговое освещение объекта складывается из трех компонентов:

  • Фоновой свет (ambient)
  • Рассеянный свет (diffuse)
  • Отраженный свет (specular)

Дополнительно к этим параметрам мы добавим собственное свечение материала (emission), этот параметр позволяет подсветить объект даже если он не освещен ни одним источником освещения.

Соответственно каждый из компонентов рассчитывается с учетом параметров источника освещения и материала объекта. Боле подробную информацию по этой модели освещения вы можете получить в этой заметке .

Расчет освещения может быть как повершинным (per-vertex lighting) так и попиксельным (per-pixel lighting). В данном уроке мы рассмотрим именно поиксельное освещение, оно позволяет сгладить недостаточную детализацию полигональных моделей и более точно рассчитать освещенность в каждой точке объекта. Основной расчет попиксельного освещения происходит во фрагментном шейдере.

Прежде чем приступать к расчету освещения необходимо рассчитать и передать некоторые параметры вершин из вершинного шейдера во фрагментный:

  • Нормаль к поверхности объекта в вершине (normal)
  • Направление падающего света, от вершины к источнику освещения (light direction)
  • Направление взгляда, от вершины к наблюдателю (view direction)
  • Расстояние от точечного источника освещения до вершины (distance)

Нормаль к поверхности объекта и направление падающего света используются для расчета рассеянного (diffuse) и отраженного (specular) света, однако для расчета отраженного света необходимо еще дополнительно знать направление взгляда наблюдателя. Расстояние от вершины до источника освещения необходимо для расчета общего коэффициента затухания (attenuation). Вершинный шейдер при этом будет таким:

#version 330 core #define VERT_POSITION 0 #define VERT_TEXCOORD 1 #define VERT_NORMAL 2 layout(location = VERT_POSITION) in vec3 position; layout(location = VERT_TEXCOORD) in vec2 texcoord; layout(location = VERT_NORMAL) in vec3 normal; // параметры преобразований uniform struct Transform { mat4 model; mat4 viewProjection; mat3 normal; vec3 viewPosition; } transform; // параметры точеченого источника освещения uniform struct PointLight { vec4 position; vec4 ambient; vec4 diffuse; vec4 specular; vec3 attenuation; } light; // параметры для фрагментного шейдера out Vertex { vec2 texcoord; vec3 normal; vec3 lightDir; vec3 viewDir; float distance; } Vert; void main(void ) { // переведем координаты вершины в мировую систему координат vec4 vertex = transform.model * vec4(position, 1 .0 ) ; // направление от вершины на источник освещения в мировой системе координат vec4 lightDir = light.position - vertex; // передадим во фрагментный шейдер некоторые параметры // передаем текстурные координаты Vert.texcoord = texcoord; // передаем нормаль в мировой системе координат Vert.normal = transform.normal * normal; // передаем направление на источник освещения Vert.lightDir = vec3(lightDir) ; // передаем направление от вершины к наблюдателю в мировой системе координат Vert.viewDir = transform.viewPosition - vec3(vertex) ; // передаем рассятоние от вершины до источника освещения Vert.distance = length(lightDir) ; // переводим координаты вершины в однородные gl_Position = transform.viewProjection * vertex; }

Освещение какого-либо пространства - это процесс, благодаря которому это пространство наполняется светом и все находящиеся в нём предметы делаются видимыми.
Освещение любого объекта зависит от двух факторов:

  • Первый - это материал, из которого сделан объект.
  • Второй - это свет, которым он освещен.

В зависимости от реализации OpenGL на сцене могут присутствовать восемь и более источников света.По умолчанию освещение отключено. Включить нулевой источник света можно командой:

  • glEnable(GL_LIGHT0);

Остальные включаются аналогичным способом, где вместо GL_LIGHT0 указывается GL_LIGHTi. После того, как источник включен, необходимо задать его параметры. Если монотонное тело у вас равномерно освещено, то вы не можете увидеть его рельеф. Поэтому нам нужно использовать источники света.
В OpenGL существует три типа источников света:

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

Для управления свойствами источника света используются команды glLight*:

  • glLightf(GLenum light, GLenum pname, GLfloat param);
    glLightfv(GLenum light, GLenum pname, const GLfloat *param);

Параметр light указывает OpenGL для какого источника света задаются параметры. Команда glLightf используется для задания скалярных параметров, а glLightfv используется для задания векторных характеристик источников света.

Сначала рассмотрим функцию, которая устанавливает базовые настройки. Когда вы разрешили освещение, то вы можете уже устанавливать фоновую освещенность. По умолчанию, значение фоновой освещенности равно (0.2, 0.2, 0.2, 1). Создайте новый проект, скопируйте туда шаблонный файл и отключите освещение. Вы с трудом сможете различить сферу на экране. С помощью функции glLightModel вы можете установить фоновое освещение. Если вы повысите его до (1,1,1,1), т.е. до максимума, то включать источники света вам не понадобится. Вы их действия просто не заметите, т.к. объект уже максимально освещен. И получится, что вы как бы отключили освещение. В общем, добавьте в main вызов следующей функции:

  • float ambient = {0.5, 0.5, 0.5, 1};
    ...
    glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient);

Попробуйте изменить параметры и посмотрите на результат.

Материал
Материал может рассеивать, отражать и излучать свет. Свойства материала устанавливаются при помощи функции

  • glMaterialfv(GLenum face, GLenum pname, GLtype* params)

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

  • GL_BACK задняя грань
    GL_FONT передняя грань
    GL_FRONT_AND_BACK обе грани

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

  • GL_AMBIENT рассеянный свет
    GL_DIFFUSE тоже рассеянный свет
    GL_SPECULAR отраженный свет
    GL_EMISSION излучаемый свет
    GL_SHININESS степень отраженного света
    GL_AMBIENT_AND_DIFFUSE оба рассеянных света

Цвет задается в виде массива из четырех элементов - RGBA. В случае GL_SHININESS params указывает на число типа float, которое должно быть в диапазоне от 0 до 128.
Вам надо всего лишь модифицировать функцию display.

  • void CALLBACK display(void)
    {
    GLUquadricObj *quadObj;
    GLfloat front_color = {0,1,0,1};
    GLfloat back_color = {0,0,1,1};
    quadObj = gluNewQuadric();
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glMaterialfv(GL_FRONT, GL_DIFFUSE, front_color);
    glMaterialfv(GL_BACK, GL_DIFFUSE, back_color);
    glPushMatrix();
    glRotated(110, -1,1,0);
    gluCylinder(quadObj, 1, 0.5, 2, 10, 10);
    glPopMatrix();
    gluDeleteQuadric(quadObj);
    auxSwapBuffers();
    }

И вы должны разрешить режим освещенности для двух граней. По умолчанию он запрещен. Добавьте в функцию main следующую строчку.

  • glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE);

Источники направленного света
Источника света такого типа находится в бесконечности и свет от него распространяется в заданном направлении. Идеально подходит для создания равномерного освещения. Хорошим примером источника направленного света может служить Солнце. У источника направленного света, кроме компонент излучения, можно задать только направление.

  • GL_POSITION (0.0, 0.0, 1.0, 0.0) //(x, y, z, w) направление источника направленного света

Первые три компоненты (x, y, z) задают вектор направления, а компонента w всегда равна нулю (иначе источник превратится в точечный).

Функции затухания
Это функция изменения интенсивности освещения(интенсивность света не убывает с расстоянием) , используется вместе с точечным освещением

  • GL_POSITION(0.0, 0.0, 1.0, 0.0)//позиция источника света (по умолчанию источник света направленный)
  • GL_CONSTANT_ATTENUATION 1.0 //постоянная k_const в функции затухания f(d)
  • GL_LINEAR_ATTENUATION 0.0 //коэффициент k_linear при линейном члене в функции затухания f(d)
  • GL_QUADRATIC_ATTENUATION 0.0 //коэффициент k_quadratic при квадрате расстояния в функции затухания f(d)

Прожекторы
Одной из разновидностей точечного источника является прожектор. Для него применимы все параметры, что и для точечного источника, но кроме того прожектор позволяет ограничить распространение света конусом. Для этого конуса можно задать коэффициент убывания интенсивности, в зависимости от угла между осью конуса и лучом распространения света.

  • GL_SPOT_DIRECTION (0.0, 0.0, -1.0) //(x, y, z) - направление прожектора (ось ограничивающего конуса)
  • GL_SPOT_CUTOFF 180.0 //угол между осью и стороной конуса (он же половина угла при вершине)
  • GL_SPOT_EXPONENT 0.0 //экспонента убывания интенсивности

Тени
Тени напрямую не поддерживаются библиотекой OpenGL, поэтому их нужно разбирать отдельно.