☁️ Комп'ютерна графіка · WebGL
📅 Березень 2026 ⏱ ≈ 10 хв читання 🟡 Середній

Об'ємний рендеринг

Поверхневий рендеринг малює трикутники. Об'ємний рендеринг крок за кроком веде промінь крізь 3D-поле густини, накопичуючи поглинуте та розсіяне світло в кожному семплі. Результат: фотореалістичні хмари, туман, вогонь і промені світла — і все це виконується у фрагментному шейдері.

1. Поглинальне середовище

Поглинальне (бере участь) середовище — це будь-який матеріал, що впливає на світло всередині свого об'єму — а не лише на поверхні. Хмари, туман, дим, каламутна вода, атмосфера, підповерхневе розсіювання у шкірі — усе це поглинальні середовища. Виникають три оптичні явища:

Коефіцієнт екстинкції σ_t = σ_a + σ_s поєднує поглинання (σ_a) та розсіювання (σ_s). Альбедо одноразового розсіювання ω = σ_s / σ_t описує, яка частка екстинкції припадає на розсіювання, а яка — на поглинання.

2. Поглинання за Бером — Ламбертом

Інтенсивність світла спадає експоненційно, поки воно проходить крізь поглинальне середовище:

Закон Бера — Ламберта T(s) = exp(−∫₀ˢ σ_t(x) dx)

T — пропускання (частка світла, що зберігається)
s — пройдена відстань
σ_t — коефіцієнт екстинкції в точці x

Для однорідного середовища (сталий σ_t) це спрощується до T = exp(−σ_t × s). Подвоєння густини або відстані → таке саме експоненційне ослаблення.

3. Трасування променя крізь об'єм

Ми не можемо аналітично проінтегрувати повне рівняння рендерингу для складних середовищ, тому дискретизуємо за допомогою трасування променя (ray marching): крокуємо вздовж променя зору N рівними кроками, семплюємо густину в кожній точці та накопичуємо колір і прозорість.

Композитинг спереду назад (кожен крок) T_accumulated = T_accumulated × exp(−σ_t × density × step_size)
color += T_accumulated × in_scatter_color × step_size

Ми крокуємо спереду назад і завчасно зупиняємося, коли пропускання падає нижче порогу (наприклад, 0,01) — промінь уже нічого не бачить далі. Це критично для продуктивності.

4. Розсіювання та фазові функції

Коли фотон розсіюється, у якому напрямку він рухається? Фазова функція p(ω_i, ω_o) описує кутовий розподіл імовірностей. Два поширені варіанти:

Ізотропне розсіювання

Однакова ймовірність у кожному напрямку. Просте, використовується для туману та щільних хмар:

Ізотропна фазова функція p = 1 / (4π)

Хеньї — Грінштейн

Стандартна модель для хмар та атмосфери. Керується параметром асиметрії g ∈ (−1, 1). Додатний g → розсіювання вперед (як хмари з ореолом навколо сонця). g = 0 → ізотропне.

Фазова функція Хеньї — Грінштейна p(cosθ, g) = (1 − g²) / (4π · (1 + g² − 2g cosθ)^(3/2))

θ — кут між падаючим і розсіяним променем
g = 0,8 — типове значення для водяних хмар

Для дволепесткових хмар (яскрава корона вперед + широке зворотне розсіювання) поєднують два доданки HG: p = lerp(HG(cosθ, g₁), HG(cosθ, g₂), blend).

5. Випромінювання (вогонь і лава)

Випромінювальні об'єми — вогонь, плазма, лава — додають до інтеграла яскравості доданок, незалежний від вхідного світла:

Об'єднано: поглинання + випромінювання L_out = L_in × T(s) + ∫₀ˢ σ_a(t) × L_e(t) × T(t) dt

L_e — випромінювана яскравість (часто з таблиці пошуку температура→колір)

На практиці: семплюйте 3D-текстуру шуму для густини, перетворіть густину на температуру, знайдіть градієнт кольору вогню та додайте це до накопиченого кольору. Пропускання за Бером — Ламбертом усе одно застосовується зверху.

6. Трасування світлового конуса (тіні)

Для об'ємних тіней усередині хмар у кожній первинній точці семплу ми випускаємо вторинний тіньовий промінь у бік джерела світла та інтегруємо густину вздовж нього, щоб обчислити пропускання від цієї точки до сонця. Це затратно — 6–16 тіньових семплів на кожен первинний семпл.

Оптимізації, що застосовуються в ігрових рушіях (Horizon Zero Dawn, Red Dead Redemption 2):

7. Реалізація на GLSL

Мінімальний фрагментний шейдер об'ємного туману (WebGL 2 / GLSL ES 3.0):

// Однорідна густина об'єму, одне напрямлене світло
uniform sampler3D u_density;  // 3D-текстура густини
uniform vec3  u_lightDir;
uniform float u_sigma_t;       // коефіцієнт екстинкції
uniform int   u_steps;         // кроки первинного променя (наприклад, 64)

// Трасування променя спереду назад
vec4 raymarch(vec3 ro, vec3 rd, float tMin, float tMax) {
  float stepSize = (tMax - tMin) / float(u_steps);
  float transmit = 1.0;
  vec3  color    = vec3(0.0);

  for (int i = 0; i < u_steps; ++i) {
    float t = tMin + (float(i) + 0.5) * stepSize;
    vec3  pos = ro + rd * t;

    float density = texture(u_density, pos * 0.5 + 0.5).r;
    if (density < 0.001) continue;

    // Пропускання за Бером — Ламбертом для цього кроку
    float extinction = u_sigma_t * density * stepSize;
    float stepTransmit = exp(-extinction);

    // Тіньовий промінь у бік світла (спрощено: один семпл)
    float shadowDensity = texture(u_density,
      (pos + u_lightDir * 0.3) * 0.5 + 0.5).r;
    float shadow = exp(-u_sigma_t * shadowDensity * 0.3);

    vec3 scatter = vec3(0.82, 0.90, 1.0) * shadow;  // відтінок неба
    color += transmit * (1.0 - stepTransmit) * scatter;
    transmit *= stepTransmit;

    if (transmit < 0.01) break;  // ранній вихід
  }
  return vec4(color, 1.0 - transmit);  // попередньо помножена альфа
}
Порада: Для переконливих хмар поле густини варто будувати з пошарового 3D-шуму Ворлі (клітинного) мінус шум Перліна на вищих частотах. Використовуйте 2D-карту покриття, щоб маскувати форми хмар, а потім ерозіюйте 3D-шумом деталей. Це відповідає моделі хмар Nubis Ендрю Шнайдера (Guerrilla Games).

8. Продуктивність GPU

Реальні продакшн-хмари: Cyberpunk 2077 використовує ~128 первинних кроків та ~6 тіньових кроків у 1/16 роздільної здатності з шахівничним патерном, амортизованих на 16 кадрах через репроєкцію. Витрати на кадр еквівалентні ~8 повним крокам у повній роздільній здатності.