«Мы не можем ожидать, что у нас получится отобразить объект точно таким, каким он является в реальности, с текстурами, тенями и т.д. Мы надеемся только на то, чтобы создать изображение, которое достаточно близко к достижению определённой степени реализма».
Буй Тыонг Фонг
Буй Тыонг Фонг родился в 1941 году и стал учёным-информатиком во время Вьетнамской войны. Должно быть, ему сложно оказалось завершить своё обучение в токсичной среде 60-х, не говоря уже о том, что его призвали на фронт! Но ему удалось выжить и дожить до 1975 года, прежде чем его жизнь забрала лейкемия спустя всего два года после того, как он заложил для мира фундамент современной теории освещения и затенения: шейдер Фонга. Вьетнамские имена состоят из трёх частей: фамилии, среднего имени и личного имени. То есть когда люди говорят «шейдер Фонга», они называют личное имя Буй Тыонга. Подробнее о личных именах можно прочитать в Википедии.
Не уверен, действительно ли это Фонг, но если верить Google, то да.
“Softly let the balmy sunshine
Play around my dying bed,
E’er the dimly lighted valley
I with lonely feet must tread. “
Let The Light Enter – стихотворение Фрэнсис Харпер
В основе шейдера Фонга лежит чрезвычайно лаконичная математика. Которую вам, на самом деле, знать не обязательно, если только вы искренне не желаете стать программистом графики. Однако в долгой перспективе её знание будет полезным. Ниже представлены выдержки из OpenGL Superbible, 7th Edition Грэма Сэллерса и Kronos Group ARB Foundation.
A) Некоторые концепции
Во-первых, давайте разберёмся с концепциями. Если вы хоть минуту имели дело с 3D-моделированием или разработкой игр, то вы наверняка с ними уже сталкивались, но повторение никому ещё не мешало.
A-1) Окружающее освещение (Ambient Light)
В большинстве книг, особенно в низкокачественных, это освещение сравнивается с солнечным светом, но это совершенно неверно. Окружающее освещение — это не солнечный свет. Оно поступает со всех направлений, то есть оно вездесуще, но в вычислениях это просто вектор с тремя составляющими. В затенении Фонга он добавляется в конце, но не изменяется.
A-2) Рассеянное/диффузное освещение (Diffuse Light)
Рассеянное освещение имеет направление. На самом деле, это направленный компонент источника освещения[sic]. В кинематографе свет рассеивается с помощью софтбокса, а в компьютерной графике освещение рассеивается с помощью формулы, которую мы покажем ниже. Величина, то есть размер рассеянного освещения зависит от поверхности. Например, если поверхность является матовой, то есть она больше поглощает, чем отражает свет, то величина будет больше, чем в случае гладкой поверхности.
Рассеяние/поглощение рассеянного освещения от матового экрана
A-3) Блики отражений (Specular Highlight)
Как и рассеянное освещение, отражённый свет является направленным, но основан на гладкости (glossiness) поверхности; он оставляет блик (highlight), который называется блеском (shininess). В реальной жизни блеск не является неотъемлемой частью материала. На самом деле, покрытие плёнкой или капля воска добавят к блеску гораздо больше, чем что-либо остальное. Отражённый блеск — это фактор, имеющий значение от 0 до 128, потому что при значениях выше 128 он не сильно будет влиять на шейдер.
Бумага с плёнкой — плотная цветная бумага с глянцевым плёночным покрытием, настоящий подарок для ребёнка.
A- 4) Альбедо (Albedo)
Это доля падающего света, отражаемая поверхностью.
A-5) Формула Фонга
Формула вычисления материала по Фонгу имеет следующий вид:
Где:
: материал окружающего освещения (Ambient material)
: материал рассеянного освещения (Diffuse material)
: материал отражённого освещения (Specular material) и : показатель блеска
: окружающее освещение
: рассеянное освещение
: отражённое освещение
Вы можете спросить, а что насчёт векторов? Не волнуйтесь, сейчас мы расскажем и о них:
: нормаль к поверхности
: единичный вектор из затеняемой точки к источнику освещения (другими словами, вектор света)
: отражение отрицательного значения вектора света
: вектор, направленный к зрителю
B) Затенение по Гуро (Gouraud Shading)
Прежде чем браться за шейдер Фонга, давайте посмотрим, как можно получить на GLSL затенение по Гуро. Учтите, что я использую версию GLSL 4.1, как и в Superbible, однако если вы фанат www.learnopengl.com, то можете пользоваться 3.3. Это не важно. Итак, давайте посмотрим, что же такое «затенение по Гуро».
Этот метод затенения был изобретён Анри Гуро в 1971 году. Он ни по каким параметрам не превосходит затенение по Фонгу, и сегодня в основном используется как мало нагружающий GPU метод предварительного просмотра в таких пакетах, как Cinema 4D. Его проблема в том, что генерируемый им блик выглядит как искра:
Эта проблема вызывается интерполяцией цветов между вершинами, а разрывы между треугольники возникают потому, что цвета интерполируются линейно. Эта проблема была решена только в шейдере Фонга. Давайте посмотрим, как можно реализовать затенение по Гуро на GLSL 4.1.
Листинг 1: повершинное затенение по Гуро на GLSL 4.1
#version 410 core
// Per-vertex inputs
layout (location = 0) in vec4 position;
layout (location = 1) in vec3 normal;
// Matrices we'll need
layout (std140) uniform constants
{
mat4 mv_matrix;
mat4 view_matrix;
mat4 proj_matrix;
};
// Light and material properties
uniform vec3 light_pos = vec3(100.0, 100.0, 100.0);
uniform vec3 diffuse_albedo = vec3(0.5, 0.2, 0.7);
uniform vec3 specular_albedo = vec3(0.7);
uniform float specular_power = 128.0;
uniform vec3 ambient = vec3(0.1, 0.1, 0.1);
// Outputs to the fragment shader
out VS_OUT
{
vec3 color;
} vs_out;
void main(void)
{
// Calculate view-space coordinate
vec4 P = mv_matrix * position;
// Calculate normal in view space
vec3 N = mat3(mv_matrix) * normal;
// Calculate view-space light vector
vec3 L = light_pos - P.xyz;
// Calculate view vector (simply the negative of the view-space position)
vec3 V = -P.xyz;
// Normalize all three vectors
N = normalize(N);
L = normalize(L);
V = normalize(V);
// Calculate R by reflecting -L around the plane defined by N
vec3 R = reflect(-L, N);
// Calculate the diffuse and specular contributions
vec3 diffuse = max(dot(N, L), 0.0) * diffuse_albedo;
vec3 specular = pow(max(dot(R, V), 0.0), specular_power) * specular_albedo;
// Send the color output to the fragment shader
vs_out.color = ambient + diffuse + specular;
// Calculate the clip-space position of each vertex
gl_Position = proj_matrix * P;
}
А теперь фрагментный шейдер.
Листинг 2: фрагментный шейдер той же концепции.
#version 410 core
// Output
layout (location = 0) out vec4 color;
// Input from vertex shader
in VS_OUT
{
vec3 color;
} fs_in;
void main(void)
{
// Write incoming color to the framebuffer
color = vec4(fs_in.color, 1.0);
}
C) Затенение по Фонгу
Прежде чем двигаться дальше, давайте запомним, что затенение по Фонгу и освещение по Фонгу — это две разные концепции. Можно избавиться от «снежинки» блика Гуро, добавив больше вершин, но зачем, если у нас есть затенение по Фонгу? В затенении по Фонгу цвет интерполируется не между вершинами (как это делается в листингах 1 и 2), мы интерполируем между вершинами нормали к поверхности, и используем сгенерированную нормаль для выполнения всех вычислений освещения для каждого пикселя, а не вершины. Однако это значит, что во фрагментном шейдере придётся выполнить больше работы, как это показано в листинге 4. Но для начала давайте рассмотрим вершинный шейдер.
Листинг 3: вершинный шейдер шейдера Фонга на GLSL 4.1.
#version 410 core
// Per-vertex inputs
layout (location = 0) in vec4 position;
layout (location = 1) in vec3 normal;
// Matrices we'll need
layout (std140) uniform constants
{
mat4 mv_matrix;
mat4 view_matrix;
mat4 proj_matrix;
};
// Inputs from vertex shader
out VS_OUT
{
vec3 N;
vec3 L;
vec3 V;
} vs_out;
// Position of light
uniform vec3 light_pos = vec3(100.0, 100.0, 100.0);
void main(void)
{
// Calculate view-space coordinate
vec4 P = mv_matrix * position;
// Calculate normal in view-space
vs_out.N = mat3(mv_matrix) * normal;
// Calculate light vector
vs_out.L = light_pos - P.xyz;
// Calculate view vector
vs_out.V = -P.xyz;
// Calculate the clip-space position of each vertex
gl_Position = proj_matrix * P;
}
Почти ничего не изменилось. Но во фрагментом шейдере ситуация совершенно другая.
Листинг 4: фрагментный шейдер затенения по Фонгу.
#version 410 core
// Output
layout (location = 0) out vec4 color;
// Input from vertex shader
in VS_OUT
{
vec3 N;
vec3 L;
vec3 V;
} fs_in;
// Material properties
uniform vec3 diffuse_albedo = vec3(0.5, 0.2, 0.7);
uniform vec3 specular_albedo = vec3(0.7);
uniform float specular_power = 128.0;
uniform vec3 ambient = vec3(0.1, 0.1, 0.1);
void main(void)
{
// Normalize the incoming N, L and V vectors
vec3 N = normalize(fs_in.N);
vec3 L = normalize(fs_in.L);
vec3 V = normalize(fs_in.V);
// Calculate R locally
vec3 R = reflect(-L, N);
// Compute the diffuse and specular components for each fragment
vec3 diffuse = max(dot(N, L), 0.0) * diffuse_albedo;
vec3 specular = pow(max(dot(R, V), 0.0), specular_power) * specular_albedo;
// Write final color to the framebuffer
color = vec4(ambient + diffuse + specular, 1.0);
}
Ну вот, на этом статья заканчивается, надеюсь, она вам понравилась. Если она зародила в вас искру интереса к OpenGL, то можете купить OpenGL Superbible на Amazon или изучите learnopengl.com. Если вы не можете разобраться с шейдерами, то рекомендую Book of Shaders.