Об'ємний рендеринг
Поверхневий рендеринг малює трикутники. Об'ємний рендеринг крок за кроком веде промінь крізь 3D-поле густини, накопичуючи поглинуте та розсіяне світло в кожному семплі. Результат: фотореалістичні хмари, туман, вогонь і промені світла — і все це виконується у фрагментному шейдері.
1. Поглинальне середовище
Поглинальне (бере участь) середовище — це будь-який матеріал, що впливає на світло всередині свого об'єму — а не лише на поверхні. Хмари, туман, дим, каламутна вода, атмосфера, підповерхневе розсіювання у шкірі — усе це поглинальні середовища. Виникають три оптичні явища:
- Поглинання — світло перетворюється на тепло. Закон Бера.
- Розсіювання назовні (out-scattering) — світло відхиляється геть від променя.
- Розсіювання всередину (in-scattering) — світло з інших напрямків відхиляється в наш промінь.
- Випромінювання — саме середовище випромінює світло (випромінювання абсолютно чорного тіла, вогонь).
Коефіцієнт екстинкції σ_t = σ_a + σ_s поєднує поглинання (σ_a) та розсіювання (σ_s). Альбедо одноразового розсіювання ω = σ_s / σ_t описує, яка частка екстинкції припадає на розсіювання, а яка — на поглинання.
2. Поглинання за Бером — Ламбертом
Інтенсивність світла спадає експоненційно, поки воно проходить крізь поглинальне середовище:
T — пропускання (частка світла, що зберігається)
s — пройдена відстань
σ_t — коефіцієнт екстинкції в точці x
Для однорідного середовища (сталий σ_t) це спрощується до
T = exp(−σ_t × s). Подвоєння густини або відстані
→ таке саме експоненційне ослаблення.
3. Трасування променя крізь об'єм
Ми не можемо аналітично проінтегрувати повне рівняння рендерингу для складних середовищ, тому дискретизуємо за допомогою трасування променя (ray marching): крокуємо вздовж променя зору N рівними кроками, семплюємо густину в кожній точці та накопичуємо колір і прозорість.
color += T_accumulated × in_scatter_color × step_size
Ми крокуємо спереду назад і завчасно зупиняємося, коли пропускання падає нижче порогу (наприклад, 0,01) — промінь уже нічого не бачить далі. Це критично для продуктивності.
4. Розсіювання та фазові функції
Коли фотон розсіюється, у якому напрямку він рухається? Фазова функція p(ω_i, ω_o) описує кутовий розподіл імовірностей. Два поширені варіанти:
Ізотропне розсіювання
Однакова ймовірність у кожному напрямку. Просте, використовується для туману та щільних хмар:
Хеньї — Грінштейн
Стандартна модель для хмар та атмосфери. Керується параметром асиметрії g ∈ (−1, 1). Додатний g → розсіювання вперед (як хмари з ореолом навколо сонця). g = 0 → ізотропне.
θ — кут між падаючим і розсіяним променем
g = 0,8 — типове значення для водяних хмар
Для дволепесткових хмар (яскрава корона вперед + широке зворотне розсіювання)
поєднують два доданки HG: p = lerp(HG(cosθ, g₁), HG(cosθ, g₂), blend).
5. Випромінювання (вогонь і лава)
Випромінювальні об'єми — вогонь, плазма, лава — додають до інтеграла яскравості доданок, незалежний від вхідного світла:
L_e — випромінювана яскравість (часто з таблиці пошуку температура→колір)
На практиці: семплюйте 3D-текстуру шуму для густини, перетворіть густину на температуру, знайдіть градієнт кольору вогню та додайте це до накопиченого кольору. Пропускання за Бером — Ламбертом усе одно застосовується зверху.
6. Трасування світлового конуса (тіні)
Для об'ємних тіней усередині хмар у кожній первинній точці семплу ми випускаємо вторинний тіньовий промінь у бік джерела світла та інтегруємо густину вздовж нього, щоб обчислити пропускання від цієї точки до сонця. Це затратно — 6–16 тіньових семплів на кожен первинний семпл.
Оптимізації, що застосовуються в ігрових рушіях (Horizon Zero Dawn, Red Dead Redemption 2):
- Репроєкція: Рендеринг у чверть роздільної здатності, репроєкція з попереднього кадру для безкоштовного часового накопичення.
- Зсуви семплів за синім шумом: Рандомізуйте початкову точку кожного проходу. TAA згладжує дрижання.
- Текстури шуму з LOD: Використовуйте дешеві 2D-карти покриття хмар, щоб пропускати трасування в порожніх ділянках неба.
- Попередньо обчислене багаторазове розсіювання: Аналітично апроксимуйте внески багаторазового розсіювання, щоб уникнути затратного трасування шляхів.
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); // попередньо помножена альфа
}
8. Продуктивність GPU
- Роздільна здатність: Рендеринг у ¼ роздільної здатності (половина ширини × половина висоти), збільшення масштабу з білатеральним/глибинно-залежним фільтром. У 4 рази дешевше.
- Часова репроєкція: Накопичення 4–8 кадрів за допомогою векторів руху. Лише 1/4 семплів на кадр мають бути свіжими.
- Пропуск порожнього простору: Побудуйте 3D-мипмап мін-макс поля густини. Повністю пропускайте кроки, коли локальна максимальна густина близька до нуля.
- Обчислювальні шейдери: Обчислювальні шейдери WebGPU можуть трасувати промені по всьому екрану за один прохід, оминаючи накладні витрати на растеризацію.
- Бюджет променів: 32–64 первинні кроки + 6–16 тіньових кроків на піксель у ¼ роздільної здатності — оптимальний варіант для реального часу (60 fps на GPU середнього класу).