OpenGL ES 2.0. Відкладене освітлення

У цій статті ми розглянемо один з варіантів реалізації відкладеного освітлення на OpenGL ES 2.0.

Deferred Lighting
Традиційний спосіб візуалізації сцени (forward rendering), передбачає видачу окремого об'єкта за один або декілька проходів, в залежності від кількості та природи оброблюваних джерел світла (на кожному проході об'єкт отримує освітлення від одного або декількох джерел). Це означає, що кількість ресурсів, витрачається на один попіксельний джерело світла, має порядок зростання O(L*N), де N кількість освітлюваних об'єктів, а L кількість освітлюваних пікселів.
Основна задача відкладеного освітлення (deferred shading/lighting/rendering) більш ефективно обробляти велику кількість джерел світла, засобами відділення прорахунку геометрії сцени від прорахунку висвітлення. Тим самим, скоротивши кількість витрачених ресурсів до O(L).
У загальному випадку, техніка складається з двох проходів
  • Geometry pass. Об'єкти відмальовує для створення буферів екранного простору (G-buffer) c глибиною, нормалями, позиціями, альбедо та ступенем дзеркальності.
  • Lighting pass. Буфери, створені на попередньому проході, що використовуються для розрахунку освітлення та отримання фінального зображення.
Для використання даної техніки в повному обсязі і побудови буферів, необхідна апаратна підтримка Multiple Render Targets (MRT).
До сожелению MRT, підтримується тільки на процесорах сумісних з OpenGL ES 3.0.
Для того, щоб обійти це апаратне обмеження ми будемо використовувати модифіковану техніку відкладеного освітлення, відому як Light Pre-Pass (LPP).
Light Pre-Pass
Отже, иходя з того, що на першому проході ми не зможемо побудувати всі необхідні, для фінального зображення, буфери, ми будуємо тільки ті, які необхідні для розрахунку освітлення. Другим проходом ми формуємо тільки буфер освітленості. І на відміну від загального випадку (де, геометрія малюється тільки один раз), ми, на третьому проході знову промальовуємо всі об'єкти, формуючи фінальне зображення. Об'єкти отримують розраховане освітлення з попереднього проходу, об'єднують його з колірними текстурами. Третій прохід компенсує те, що ми не змогли (у силу апаратних обмежень) зробити на першому.
  • Geometry pass. Об'єкти відмальовує для створення буферів екранного простору тільки з глибиною і нормалями.
  • Lighting pass. Будується буфер освітленості. Для розрахунку освітленості, крім даних з попереднього проходу (буфери глибини і нормалей), нам також необхідна тривимірна позиція пікселя. Техніка передбачає відновлення позиції з буфера глибини.
  • Final pass. Об'єкти відмальовує для створення фінального зображення, на основі буфера освітленості і кольорових текстур об'єктів.
Перш ніж детально розібрати кожен прохід, слід згадати додаткові обмеження, що накладаються на буфери, в рамках OpenGL ES 2.0. Нам потрібно поддрержка двох додаткових розширення OES_rgb8_rgba8, OES_packed_depth_stencil.
  • OES_rgb8_rgba8. Дозволить нам підтримувати в якості буферів, текстури з форматом пикесельных даних RGBA8. Такі текстури ми будемо використовувати в якості буфера нормалей і буфера освітленості.
  • OES_packed_depth_stencil. Дозволить нам використовувати буфер глибини разом з буфером трафарету і форматом піксельних даних UNSIGNED_INT_24_8_OES. (24 біта — глибина, 8 біт — значення трафарету). Буфер траферета знадобиться на другому етапі для оптимізації. Також, за 24х бітного буфера глибини нам знадобиться підтримка обчислень з високою точністю у фрагментном шейдере.
На даний момент ці розширення підтримують понад 95% існуючих пристроїв.
Geometry pass
Створення буфера для закадрової відтворення
// Створюємо текстуру для буфера глибини
glBindTexture(GL_TEXTURE_2D, shared_depth_buffer);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24_OES, cx, cy, 0,
GL_DEPTH_STENCIL_OES, GL_UNSIGNED_INT_24_8_OES, nullptr);
glBindTexture(GL_TEXTURE_2D, 0);

// створюємо текстуру для буфера нормалей
glBindTexture(GL_TEXTURE_2D, normal_buffer);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, cx, cy, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindTexture(GL_TEXTURE_2D, 0);

// Прив'язуємо текстури до фреймбуферу проходу.
glBindFramebuffer(GL_FRAMEBUFFER, pass_fbo);
// Прив'язуємо текстуру в якості колірного буфера
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
GL_TEXTURE_2D, normal_buffer, 0);
// Прив'язуємо тестуру в якості буфера глибини.
// На цю ж текстуру ми прив'яжемо в якості буфера глибини і буфера трафарету у другому проході
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
GL_TEXTURE_2D, shared_depth_buffer, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

Растеризація.
// Вершинний шейдер
attribute vec3 a_vertex_pos;
attribute vec3 a_vertex_normal;

uniform mat4 u_matrix_mvp;
uniform mat4 u_matrix_model_view;

varying vec3 v_normal;

void main()
{
gl_Position = u_matrix_mvp * vec4(a_vertex_pos, 1.0);
v_normal = vec3(u_matrix_model_view * vec4(a_vertex_normal, 0.0));
}

// Фрагментний шейдер
precision lowp float;

varying vec3 v_normal;

void main()
{
// зберігаємо нормаль до видових координатах 
gl_FragColor = vec4(v_normal * 0.5 + 0.5, 1.0);
}

Результат

Lighting pass
Створення буфера для закадрової відтворення
// тектура для буфера освітленості створюється також як для буфера нормалей на першому проході
...
// Прив'язуємо текстури до фреймбуферу проходу.
glBindFramebuffer(GL_FRAMEBUFFER, pass_fbo);
// Прив'язуємо текстуру в якості колірного буфера
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
GL_TEXTURE_2D, light_buffer, 0);
// Прив'язуємо тестуру в якості буфера глибини і буфера трафарету.
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
GL_TEXTURE_2D, shared_depth_buffer, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT,
GL_TEXTURE_2D, shared_depth_buffer, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

Отрисовка
На цьому етапі ми будемо перетворювати кожне джерело світла окремо. Щоб промалювати джерело, необхідно якимось чином висловити його обсяг. Це можна робити по-різному, з допомогою екранно-орієнтованих прямокутників, кубів, сфер.
В якості апроксимації світлових обсягів точкових джерел світла ми будемо використовувати примітив під назвою Icosphere

Шейдери
// Вершинний шейдер
attribute vec3 a_vertex_pos;
uniform mat4 u_matrix_mvp;
varying vec4 v_pos;

void main()
{
v_pos = u_matrix_mvp * vec4(a_vertex_pos, 1.0);
gl_Position = v_pos;
}

// Фрагментний шейдер
precision highp float;

// ближня січну площину
uniform float u_camera_near;
// дальня січну площину
uniform float u_camera_far;
// параметри для відновлення тривимірної позиції пискселя по глибині
// u_camera_view_param.x = tan(fov / 2.0) * aspect;
// u_camera_view_param.y = tan(fov / 2.0);
uniform vec2 u_camera_view_param;

// u_light_inv_range_square = 1.0 / light_radius^2
// використовується для розрахунку ослаблення світла
uniform float u_light_inv_range_square;
// інтенсивність світла
uniform vec3 u_light_intensity;
// позиція джерела в видових координатах
uniform vec3 u_light_pos;

// буфер нормалей
uniform sampler2D u_map_geometry;
// буфер глибини
uniform sampler2D u_map_depth;

varying vec4 v_pos;

const float shininess = 32.0;

// розраховуємо ослаблення світла
float fn_get_attenuation(vec3 pos)
{
vec3 direction = u_light_pos - pos;
float value = dot(direction, direction) * u_light_inv_range_square;
return 1.0 - clamp(value, 0.0, 1.0);
}

// отримуємо значення глибини в діапазоні від near до far
float fn_get_linearize_depth(float depth)
{
return 2.0 * u_camera_near * u_camera_far /
(u_camera_far + u_camera_near -
(depth * 2.0 - 1.0) * (u_camera_far - u_camera_near));
}

// отримуємо текстурні координати для доступу до буферам
vec2 fn_get_uv(vec4 pos)
{
return (pos.xy / pos.w) * 0.5 + 0.5;
}

// отримуємо нормаль
vec3 fn_get_view_normal(vec2 uv)
{
return texture2D(u_map_geometry, uv).xyz * 2.0 - 1.0;
}

// відновлюємо тривимірну позицію пікселя з буфера глибини
vec3 fn_get_view_pos(vec2 uv)
{
float depth = texture2D(u_map_depth, uv).x;
depth = fn_get_linearize_depth(depth);
return vec3(u_camera_view_param * (uv * 2.0 - 1.0) * depth, -depth);
}

void main()
{
// розраховуємо освітлення
vec2 uv = fn_get_uv(v_pos);
vec3 normal = fn_get_view_normal(uv);
vec3 pos = fn_get_view_pos(uv);
float attenuation = fn_get_attenuation(pos);

vec3 lightdir = normalize(u_light_pos - pos);
float nl = dot(normal, lightdir) * attenuation;

vec3 reflectdir = reflect(lightdir, normal);
float spec = pow(clamp(dot(normalize(pos), reflectdir), 0.0, 1.0), shininess);

gl_FragColor = vec4(u_light_intensity * nl, spec * nl);
}

Результат для одного джерела.

Ліворуч показаний обсяг — це пікселі, для яких була розрахована освітленість. Праворуч показані пікселі, які фактично отримали освітленість. Можна помітити, що освітлених пікселів набагато менше, ніж тих, які брали участь в розрахунках.
Для вирішення цієї проблеми ми скористаємося буфером трафарету, за допомогою якого по глибині відсічемо непотрібні пікселі.
Тоді весь процес відтворення джерела свєти буде виглядати так:
// 0. включаємо тест трафарету і забороняємо запис в буфер глибини 
// (загальні дії для відтворення всіх джерел)
glEnable(GL_STENCIL_TEST);
glDepthMask(GL_FALSE);

// налаштовуємо параметри і заповнюємо буфер трафарету
// ------------------------------------------------------------------------
// 1. включаємо тест глибини
glEnable(GL_DEPTH_TEST)
// 2. забороняємо запис в колірній буфер, ми будемо заповнювати тільки буфер трафарету
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
// 3. очищаємо трафарет. Буфер заповнюється для кожного джерела
glClear(GL_STENCIL_BUFFER_BIT);
// 4. налаштовуємо опції заповнення трафарету
glStencilFunc(GL_ALWAYS, 0, 0);
glStencilOpSeparate(GL_BACK, GL_KEEP, GL_INCR_WRAP, GL_KEEP);
glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_DECR_WRAP, GL_KEEP);
// 5. Промальовуємо обсяг
...

// 6. Дозволяємо обробляти лише ті пікселі, 
// значення буфера траферета для яких, не дорівнює нулю
glStencilFunc(GL_NOTEQUAL, 0, 0xFF);
// 7. Вимикаємо тест глибини
glDisable(GL_DEPTH_TEST);
// 8. Дозволяємо запис в колірній буфер
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

// промальовуємо джерело світла
// ----------------------------------------------------------------------
// 1. Дозволяємо можливість виключати з відтворення передню або задню поверхні полігонів
glEnable(GL_CULL_FACE);
// 2. Виключаємо з відтворення передню поверхню полігонів
glCullFace(GL_FRONT);
// 3. Промальовуємо обсяг (додаємо його в буфер освітленості адитивным змішуванням).
// Пікселі, які не пройшли тест траферета будуть відкинуті
...
// 4. Повертаємо виключення за промовчанням
glCullFace(GL_BACK);
// 5. Відключаємо можливість виключення
glDisable(GL_CULL_FACE);

Шейдери для відтворення об'єму в буфер трафарету
// Вершинний шейдер
attribute vec3 a_vertex_pos;
uniform mat4 u_matrix_mvp;

void main()
{
gl_Position = u_matrix_mvp * vec4(a_vertex_pos, 1.0);
}

// Фрагментний шейдер
precision lowp float;
void main()
{
}

Результат

На додаток до всього, зауважимо, з урахуванням того, що буфер траферета заповнюється без винятку поверхонь, а освітленість розраховується тільки за заднім, це також вирішує проблему відтворення джерела світла, якщо всередині знаходиться камера.
Final pass
Заключним проходом ми ще раз промальовуємо всі об'єкти, відновлюючи освітленість пікселів з буфера з попереднього проходу.
// Вершинний шейдер
attribute vec3 a_vertex_pos;
uniform mat4 u_matrix_mvp;

varying vec4 v_pos;

void main()
{
v_pos = u_matrix_mvp * vec4(a_vertex_pos, 1.0);
gl_Position = v_pos;
}

// Фрагментний шейдер
precision lowp float;

uniform sampler2D u_map_light;
uniform vec3 u_color_diffuse;
uniform vec3 u_color_specular;

varying vec4 v_pos;

vec2 fn_get_uv(vec4 pos)
{
return (pos.xy / pos.w) * 0.5 + 0.5;
}

void main()
{
vec4 color_light = texture2D(u_map_light, fn_get_uv(v_pos));

vec3 color = color_light.rgb;
gl_FragColor = vec4(u_color_diffuse * color + 
u_color_specular * (color * color_light.a), 1.0);
}



Код проекту можна знайти на GitHub.
Буду радий коментарям та пропозиціям (можна поштою yegorov.alex@gmail.com)
Спасибі!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.