Увімкнення джерел світла в opengl. Освітлення в OpenGL. Що таке освітлення по Фонгу

Для створення реалістичних зображень необхідно визначити як властивості самого об'єкта, так і властивості середовища, в якому він знаходиться. Перша група властивостей включає параметри матеріалу, з якого зроблено об'єкт, способи нанесення текстури на його поверхню, ступінь прозорості об'єкта. До другої групи можна віднести кількість та властивості джерел світла, рівень прозорості середовища. Всі ці властивості можна задавати за допомогою відповідних команд OpenGL.

Властивості матеріалу

Для встановлення параметрів поточного матеріалу використовуються команди

void glMaterial(GLenum face, GLenum pname, GLtype param)
void glMaterialv(GLenum face, GLenum pname, GLtype *params)

З їх допомогою можна визначити розсіяний, дифузний та дзеркальний кольори матеріалу, а також колір ступінь дзеркального відображення та інтенсивність випромінювання світла, якщо об'єкт повинен світитися. Який саме параметр визначатиметься значенням param, залежить від значення pname:

GL_AMBIENTпараметр params повинен містити чотири цілі або речові значення кольорів RGBA, які визначають розсіяний колір матеріалу (колір матеріалу в тіні).

Значення за промовчанням: (0.2, 0.2, 0.2, 1.0).

GL_DIFFUSEпараметр params повинен містити чотири цілі або речові значення кольорів RGBA, які визначають колір дифузного відображення матеріалу.

Значення за промовчанням: (0.8, 0.8, 0.8, 1.0).

GL_SPECULARпараметр params повинен містити чотири цілі або речові значення кольорів RGBA, які визначають колір дзеркального відображення матеріалу.

GL_SHININESSпараметр params повинен містити одне ціле або речове значення в діапазоні від 0 до 128, яке визначає ступінь дзеркального відображення матеріалу.

Значення за промовчанням: 0.

GL_EMISSIONпараметр params повинен містити чотири цілі або речові значення кольорів RGBA, які визначають інтенсивність випромінюваного світла матеріалу.

Значення за промовчанням: (0.0, 0.0, 0.0, 1.0).

GL_AMBIENT_AND_DIFFUSEеквівалентно двом викликам команди glMaterial…() зі значенням pname GL_AMBIENT та GL_DIFFUSE та однаковими значеннями params.

Це означає, що виклик команди glMaterial() можливий лише для встановлення ступеня дзеркального відображення матеріалу. У більшості моделей враховується дифузне та дзеркальне відбите світло; перший визначає природний колір об'єкта, а другий - розмір та форму відблисків на його поверхні.

Параметр face визначає тип граней, для яких визначається цей матеріал і може приймати значення GL_FRONT, GL_BACK або GL_FRONT_AND_BACK.

Якщо в сцені матеріали об'єктів відрізняються лише одним параметром, рекомендується спочатку встановити потрібний режим, викликавши glEnable() c параметром GL_COLOR_MATERIAL, а потім використовувати команду

void glColorMaterial(GLenum face, GLenum pname)

де параметр face має аналогічний зміст, а параметр pname може набувати всіх перелічених значень. Після цього значення вибраного за допомогою pname властивості матеріалу для конкретного об'єкта (або вершини) встановлюється викликом команди glColor…(), що дозволяє уникнути викликів більш ресурсомісткої команди glMaterial…() та підвищує ефективність програми.

Додати в сцену джерело світла можна за допомогою команд

void glLight(GLenum light, GLenum pname, GLfloat param)
void glLight(GLenum light, GLenum pname, GLfloat *params)

Параметр light однозначно визначає джерело і вибирається з набору спеціальних символічних імен виду GL_LIGHTi, де i повинно лежати в діапазоні від 0 до GL_MAX_LIGHT, яке не перевищує восьми.

Два параметри, що залишилися, мають аналогічний зміст, що і в команді glMaterial…(). Розглянемо їх призначення (спочатку описуються параметри першої команди, потім другий):

GL_SPOT_EXPONENTпараметр param повинен містити ціле чи речове число від 0 до 128, що задає розподіл інтенсивності світла. Цей параметр визначає рівень сфокусованості джерела світла.

Значення за замовчуванням: 0 (розсіяне світло).

GL_SPOT_CUTOFFпараметр param повинен містити ціле або речове число між 0 і 90 або 180, яке визначає максимальний кут розкиду світла. Значення цього параметра є половина кута на вершині конусовидного світлового потоку, створюваного джерелом.

Значення за замовчуванням: 180 (розсіяне світло).

GL_AMBIENTпараметр params повинен містити чотири цілі або речові значення кольорів RGBA, які визначають колір фонового освітлення.

Значення за промовчанням: (0.0, 0.0, 0.0, 1.0).

GL_DIFFUSEпараметр params повинен містити чотири цілі або речові значення кольорів RGBA, які визначають колір дифузного освітлення.

GL_SPECULARпараметр params повинен містити чотири цілі або речові значення кольорів RGBA, які визначають колір дзеркального відображення.

Значення за замовчуванням: (1.0, 1.0, 1.0, 1.0) для LIGHT0 та (0.0, 0.0, 0.0, 1.0) для інших.

GL_POSITIONпараметр params повинен містити чотири цілі або речові, які визначають положення джерела світла. Якщо значення компоненти w дорівнює 0.0, то джерело вважається нескінченно віддаленим і при розрахунку освітленості враховується лише напрямок на точку (x, y, z), інакше вважається, що джерело розташоване в точці (x, y, z, w).

Значення за промовчанням: (0.0, 0.0, 1.0, 0.0).

GL_SPOT_DIRECTIONпараметр params повинен містити чотири цілі або речові числа, які визначають напрям світла.

Значення за промовчанням: (0.0, 0.0, -1.0, 1.0).

При зміні положення джерела світла слід враховувати такі факти: якщо положення задається командою glLight…() перед визначенням орієнтації погляду (командою glLookAt()), вважатиметься, що джерело перебуває у точці спостереження. Якщо положення встановлюється між завданням орієнтації та перетвореннями видової матриці, воно фіксується і залежить від видових перетворень. В останньому випадку, коли положення встановлено після орієнтації та видової матриці, його положення можна змінювати, встановлюючи як нову орієнтацію спостерігача, так і змінюючи видову матрицю.

Для використання освітлення спочатку потрібно встановити відповідний режим викликом команди glEnable (GL_LIGHTNING), а потім увімкнути потрібне джерело командою glEnable (GL_LIGHTn).

Модель освітлення

В OpenGL використовується модель освітлення Фонга, відповідно до якої колір точки визначається декількома факторами: властивостями матеріалу та текстури, величиною нормалі у цій точці, а також положенням джерела світла та спостерігача. Для коректного розрахунку освітленості в точці треба використовувати одиничні нормалі, проте команди типу glScale…() можуть змінювати довжину нормалей. Щоб це враховувати, використовується вже згадуваний режим нормалізації нормалей, що включається викликом команди glEnable(GL_NORMALIZE).

Для завдання глобальних параметрів освітлення використовуються команди

void glLightModel(GLenum pname, GLenum param)
void glLightModelv(GLenum pname, const GLtype *params)

Аргумент pname визначає, який параметр моделі освітлення налаштовуватиметься і може приймати наступні значення:

GL_LIGHT_MODEL_LOCAL_VIEWERпараметр param має бути булевським і задає положення спостерігача. Якщо він дорівнює FALSE, то напрямок огляду вважається паралельним осі -z, незалежно від положення у видових координатах. Якщо він дорівнює TRUE, то спостерігач знаходиться на початку видової системи координат. Це може покращити якість освітлення, але ускладнює його розрахунок.

Значення за промовчанням: FALSE.

GL_LIGHT_MODEL_TWO_SIDEпараметр param має бути булевским і управляє режимом розрахунку освітленості як лицьових, так зворотних граней. Якщо він дорівнює FALSE, то освітленість розраховується лише для лицьових граней. Якщо він дорівнює TRUE, розрахунок проводиться й у зворотних граней. Значення за промовчанням: FALSE.

GL_LIGHT_MODEL_AMBIENTпараметр params повинен містити чотири цілі або речові числа, які визначають колір фонового освітлення навіть у разі відсутності певних джерел світла.

Значення за промовчанням: (0.2, 0.2, 0.2, 1.0).

Що ж, панове. Останнім часом ми досить багато дізналися про 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 correctod 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)),
матеріалSpecularFactor
) ;
vec4 specularColor = light. specularIntensity * vec4 (light. color, 1)
* materialSpecularIntensity * specularFactor;
// ...

Відбите освітлення. Змінна lightReflect - це одиничний вектор, що задає напрямок відбитого світла. Змінна спеціальнафактор обчислюється схожим на дифузнийфактор способом, тільки на цей раз враховується косинус кута між напрямком, в якому відобразилося світло, і напрямком від фрагмента до камери. Якщо цей кут дорівнює нулю, значить відбите світло летить прямо в камеру і відблиски на поверхні максимальні. Якщо кут великий, значить ніяких відблисків не повинно бути видно. Тут матеріал SpecialFactor є uniform змінною. Чим вона більша, тим менше за площею відблиски на поверхні об'єкта. Також використовується змінна матеріал SpecialIntensity, що визначає яскравість відблисків. Зверніть увагу, що все це - властивості матеріалу, а не світла. Наприклад, метал відбиває світло, і тому має відблиски. А дерево світло не відбиває, і потім ви ніколи не бачите відблисків на деревах (звичайно, якщо поверхня суха, і таке інше).

У наведеному коді світла має властивість особливоїінтенсивності. Але його слід використовувати тільки з метою налагодження, щоб підкреслити відблиски від певного джерела світла. У релізній версії коду цей коефіцієнт повинен або дорівнювати одиниці, або бути викинутим з коду.

Нарешті, три компоненти складаються та повертається результат:

// ...

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 у дільнику додатково додається одиниця, щоб запобігти можливості поділу на нуль. Після цього все обчислюється так само, як для спрямованого світла.

Прожектор (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 correctod 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
}

На матеріалемісії не звертайте особливої ​​уваги. Це ще одна властивість матеріалу, що додає йому самостійне світіння. Багато об'єктів світяться самі собою. Взяти ті ж лампочки, які є джерелом світла для інших об'єктів. Ми повинні бачити їх у повній темряві, навіть якщо лампочки не освітлені жодним іншим джерелом світла, вірно?

Що дійсно заслуговує на увагу — це гамма-корекція, яка полягає у зведенні всіх компонентів світла в ступінь 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 властивістю як джерела світла, а й матеріалу. Не впевнений, щоправда, наскільки все це стосується реалістичності освітлення. Але як домашнього завданняможете грати з усім цим.

Цього року ми будемо вчитися висвітлювати і затіняти наші 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.

Частина 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); width, height), glMatrixMode(GL_PROJECTION), glLoadIdentity(), glOrtho(-1.2, 1.2, -1.2, 1.2, -1, 1); (0.4, 0.7, 0.2); float light0_direction = (0.0, 0.0, 1.0, 0.0); 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);< 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*()– це команда, яка використовується для встановлення всіх параметрів моделі освітлення. Може приймати два аргументи: ім'я параметра моделі освітлення як константи і значення цього параметра.

Код:


Тепер ви можете створювати чудові джерела світла.
Поділіться з друзями або збережіть для себе:

Завантаження...