ReSTIR: просторово-часове ресемплування за важливістю на основі резервуарів
ReSTIR — це алгоритм семплування, що уможливив глобальне освітлення реального часу з тисячами динамічних джерел світла на 60 fps на споживчому обладнанні. Він досягає цього, поєднуючи зважене резервуарне семплування з просторовим та часовим повторним використанням сусідів — фактично дозволяючи кожному пікселю «позичати» хороших кандидатів-семплів у своїх сусідів у просторі та часі, помножуючи ефективну кількість семплів на порядки величини без додаткової вартості променів.
1. Проблема багатьох джерел світла
Рівняння рендерингу вимагає інтегрування вхідної яскравості по всіх напрямках у кожній точці поверхні:
У сценах з тисячами динамічних емісійних сіток (площинні джерела світла, емісійні частинки, автомобільні фари, вікна в нічній сцені міста) наївне рівномірне семплування одного джерела світла з усієї множини дає дисперсію, пропорційну N_lights. Для N = 10 000 джерел та 1 семпла на піксель майже кожен семпл потрапить на джерело, що дає мізерний внесок у освітлення поточного пікселя — продукуючи надзвичайно шумні результати.
Ідеалом було б семплювати джерела пропорційно до їхнього внеску p̂(x) = f_r · L_e · |cos θ| / r² — але обчислення цього для кожного джерела × кожного пікселя становить O(N_pixels × N_lights) на кадр, далеко за межами бюджету реального часу.
Попередні роботи: дерева світла та віртуальні точкові джерела
- Дерева світла (Lightcuts, 2005): ієрархічне кластерування джерел; обхід дерева на піксель з вибором O(log N) представників. Працює для статичних сцен, але має попіксельну вартість O(log N).
- Віртуальні точкові джерела (VPL/Instant Radiosity): вторинні джерела, згенеровані з RSM. Не опрацьовує глянсові поверхні; обмеження вносить зміщення.
- Резервуари (ReSTIR, 2020): попіксельна вартість O(1) з ефективною кількістю семплів, що зростає квадратично через повторне використання сусідів.
2. Зважене резервуарне семплування
Зважене резервуарне семплування (WRS) (Vitter, 1985) дає один семпл із потоку кандидатів, де кожен елемент xᵢ має вагу wᵢ — без потреби зберігати всі елементи:
Ключова властивість: після обробки n кандидатів резервуар містить рівно 1 семпл, і P(y = xᵢ) = wᵢ / Σwⱼ. Резервуар можна оновлювати інкрементно — нових кандидатів можна об'єднувати за O(1). Саме це робить ReSTIR дружнім до GPU: кожен піксель підтримує один резервуар у текстурі.
Об'єднання двох резервуарів
Два резервуари R₁ та R₂ (кожен обробив M₁ та M₂ кандидатів) можна об'єднати в один незміщений резервуар:
Ця операція об'єднання — основа просторового повторного використання: резервуари сусідніх пікселів об'єднуються в резервуар поточного пікселя, фактично розширюючи його множину кандидатів без переобчислення жодних BRDF.
3. Ресемплування за важливістю (RIS)
Ресемплування за важливістю (RIS) пов'язує WRS із рендерингом методом Монте-Карло. Ми хочемо оцінити:
RIS генерує M кандидатів {xᵢ} з вихідної pdf p(x), призначає вагу wᵢ = p̂(xᵢ)/p(xᵢ), запускає WRS для вибору одного семпла z, потім оцінює I як:
Критично, що цільова PDF p̂ може апроксимувати ідеальну важливість, пропорційну f, але не зобов'язана бути точно p ∝ f. Навіть груба апроксимація (наприклад, p̂ = L_e/r² без урахування BRDF) кардинально зменшує дисперсію порівняно з рівномірним семплуванням. BRDF обчислюється лише раз для вибраного кандидата z, а не для всіх M кандидатів — утримуючи попіксельну вартість на рівні O(M_initial + 1 обчислення BRDF).
4. Часове повторне використання
У рендерері реального часу кадри корельовані: сцена змінюється мало між кадром n та кадром n−1. ReSTIR використовує це, зберігаючи один резервуар на піксель і повторно використовуючи його між кадрами:
За M_new = 32 початкових кандидатів та часового накопичення, обмеженого M = 32 × 20 = 640 ефективними кандидатами, кожен піксель має перевагу семплування 640 кандидатів ціною генерування лише 32 — 20-кратне зменшення дисперсії безкоштовно.
Репроєкція та розкриття
Репроєкція зазнає невдачі, коли: (1) геометрія під пікселем змінюється (розкриття), (2) нормаль поверхні чи матеріал змінюються (нестабільність геометрії) або (3) раптово з'являється яскраве джерело світла (часове відставання). Усі три виявляються через:
- Тест глибини: |depth_prev − depth_curr| / depth_curr > поріг → скинути історію
- Тест нормалі: cos(n_prev · n_curr) < 0.9 → скинути історію
- Вектори руху з G-буфера для точної субпіксельної репроєкції
5. Просторове повторне використання та зміщення
Після часового повторного використання резервуар кожного пікселя додатково об'єднується з k випадково вибраними сусідами (типово k = 5):
Просторове повторне використання фактично помножує бюджет семплів на k+1 — але воно вносить зміщення, якщо його не скоригувати. Зміщення виникає тому, що сусідній піксель q_j може мати іншу видиму поверхню, ніж q: семпл світла y, вибраний у q_j, може бути затулений або геометрично неузгоджений у точці поверхні q. Використання w_sum резервуара R_q_j (обчисленого на поверхні q_j), наче він дійсний у q, переоцінює внесок.
6. Корекція зміщення та ваги MIS
Оригінальна стаття ReSTIR DI (Bitterli et al., SIGGRAPH 2020) запропонувала два підходи до корекції зміщення:
Метод 1: зі зміщенням (наближений)
Прийми зміщення та зауваж, що воно переважно проявляється поблизу меж тіней. Для багатьох застосувань (ігри, превізуалізація реального часу) зміщення візуально прийнятне й дає найпростішу реалізацію.
Метод 2: незміщена корекція на основі MIS
Заміни w_sum кожного резервуара версією, зваженою за MIS, що враховує імовірність того, що вибраний семпл був би вибраний усіма резервуарами-учасниками:
Альтернативний незміщений підхід (Wyman & Panteleev, 2021 — «Rearchitecting Spatiotemporal Resampling for Production») уникає зайвих тіньових променів, використовуючи лише тест геометричної подібності замість повної видимості. Це вносить помірне зміщення на межах затулення, але значно швидше.
Ваги впевненості
Практичне ускладнення: після часового накопичення R_prev.M може бути дуже великим (тисячі), домінуючи у просторовому об'єднанні. Bitterli et al. рекомендують обмежувати M, щоб запобігти домінуванню одного резервуара над усіма сусідами:
7. ReSTIR DI, GI та PT
Фреймворк ReSTIR було розширено від прямого освітлення до повного глобального освітлення:
ReSTIR DI (пряме освітлення)
Оригінальна стаття 2020 року. Семпли: одне площинне джерело світла на резервуар. Цільова PDF: p̂ = L_e · G · |cos θ| / r² (без тіней). Тіньовий промінь обчислюється лише для кінцевого вибраного кандидата. Досягає якості, еквівалентної 128 кандидатам світла на піксель, ціною 1 тіньового променя на піксель з просторовим+часовим повторним використанням.
Постачається в RTX Direct Illumination SDK від NVIDIA (RTXDI) та широко використовується в іграх та офлайн-інструментах превізуалізації з 2021 року.
ReSTIR GI (глобальне освітлення)
Ouyang et al. (SIGGRAPH 2021). Розширює ReSTIR до сегментів шляхів: замість семплування прямого світла кожен резервуар зберігає повний шлях першого відбиття (x_v → x_s, де x_s — точка вторинного влучання з її яскравістю L_i(x_s)). Цільова PDF — p̂(x_s) = L_i(x_s) · f_r(x_v → x_s) · |cos θ|.
ReSTIR GI досягає якості GI з 1 відбиттям, порівнянної з 16 spp у одному відбитті на швидкостях реального часу. Він живить режим «Lumen Hardware Lumen» в Unreal Engine 5.2+.
ReSTIR PT (трасування шляхів)
Lin et al., SIGGRAPH 2022. Розширює до повних шляхів довільної глибини. Кожен резервуар зберігає цілий світловий шлях як послідовність вершин {x₁, x₂, ..., xₖ}. Відображення зсуву з переприєднанням (натхненне трасуванням шляхів MCMC) дозволяє сусіднім пікселям ділити префікси шляхів, водночас коректно зсуваючи суфікс шляху до нового початку.
ReSTIR PT досягає когерентного глобального освітлення (каустики, взаємовідбиття, дзеркальні ланцюги) на еквіваленті 1–4 spp, уможливлюючи трасування шляхів офлайн-якості на споживчих GPU майже в реальному часі (1–10 fps для продакшн-сцен).
| Варіант | Рік | Семпл резервуара | Ефективний еквів. spp | Ключова вартість |
|---|---|---|---|---|
| ReSTIR DI | 2020 | 1 кандидат площинного світла | 128–512 spp прямого світла | 1 тіньовий промінь/піксель |
| ReSTIR GI | 2021 | 1 вторинне влучання шляху | 8–32 spp GI першого відбиття | 1 вторинний промінь/піксель |
| ReSTIR PT | 2022 | Повний шлях з k відбиттями | 4–16 spp повного шляху | 1 первинний + k променів переприєднання |
8. Нотатки щодо реалізації у WebGPU
Мінімальна реалізація ReSTIR DI у WebGPU потребує таких проходів рендерингу на кадр:
Структура резервуара у WGSL
// Запис буфера резервуара у WGSL
struct Reservoir {
y_light_index : u32, // індекс вибраного кандидата світла
y_uv : vec2f, // UV на поверхні світла (для площинних джерел)
w_sum : f32, // накопичена сума ваг
M : u32, // кількість побачених кандидатів
W : f32, // незміщена вага: w_sum / (M · p̂(y))
};
@group(0) @binding(0) var<storage, read_write> reservoirs : array<Reservoir>;
@group(0) @binding(1) var<storage, read> lights : array<Light>;
@group(0) @binding(2) var g_depth : texture_2d<f32>;
@group(0) @binding(3) var g_normal : texture_2d<f32>;
// Оновлення WRS (виклик для кожного кандидата xᵢ з вагою wᵢ)
fn wrs_update(r: ptr<function, Reservoir>, x: u32, w: f32, rng: f32) {
(*r).w_sum += w;
(*r).M += 1u;
if rng < w / (*r).w_sum {
(*r).y_light_index = x;
}
}
// Об'єднання двох резервуарів (для просторового/часового поєднання)
fn merge(dst: ptr<function, Reservoir>, src: Reservoir,
p_hat_src: f32, rng: f32) {
let w = p_hat_src * src.W * f32(src.M);
wrs_update(dst, src.y_light_index, w, rng);
(*dst).M += src.M;
}
// Обчислювальний шейдер: генерація початкових кандидатів
@compute @workgroup_size(8, 8)
fn generate_initial(@builtin(global_invocation_id) gid: vec3u) {
let px = gid.xy;
let res_idx = px.y * u32(screen_size.x) + px.x;
var r : Reservoir;
r.w_sum = 0.0; r.M = 0u; r.W = 0.0;
// Читаємо поверхню G-буфера в цьому пікселі
let depth = textureLoad(g_depth, px, 0).r;
let normal = textureLoad(g_normal, px, 0).xyz * 2.0 - 1.0;
let world_pos = reconstruct_world(px, depth);
var rng = pcg_hash(res_idx ^ (frame_index * 1013904223u));
for (var i = 0u; i < INITIAL_CANDIDATES; i++) {
let light_idx = rand_u32(&rng) % num_lights;
let light = lights[light_idx];
// Цільова PDF без тіней: L_e · |cos θ| / r²
let to_light = light.position - world_pos;
let dist2 = dot(to_light, to_light);
let n_dot_l = max(0.0, dot(normal, normalize(to_light)));
let p_hat = length(light.emission) * n_dot_l / dist2;
let w = p_hat / (1.0 / f32(num_lights)); // вага = p̂ / source_pdf
wrs_update(&r, light_idx, w, fract(rand_f32(&rng)));
}
// Обчислюємо незміщену вагу W = w_sum / (M · p̂(y))
let sel = lights[r.y_light_index];
let to_sel = sel.position - world_pos;
let dist2s = dot(to_sel, to_sel);
let p_hat_y = length(sel.emission) * max(0.0, dot(normal, normalize(to_sel))) / dist2s;
r.W = select(0.0, r.w_sum / (f32(r.M) * p_hat_y), p_hat_y > 0.0);
reservoirs[res_idx] = r;
}