Skip to content

Commit e5aa941

Browse files
authored
Solari initial GI (#20020)
# Objective - Add 1-bounce RT GI ## Solution - Implement a very very basic version of ReSTIR GI https://d1qx31qr3h6wln.cloudfront.net/publications/ReSTIR%20GI.pdf - Pretty much a copy of the ReSTIR DI code, but adjusted for GI. - Didn't implement add more spatial samples, or do anything needed for better quality. - Didn't try to improve perf at all yet (it's actually faster than DI though, unfortunately 😅) - Didn't spend any time cleaning up the shared abstractions between DI/GI --- ## Showcase <img width="2564" height="1500" alt="image" src="https://github.com/user-attachments/assets/1ff7be1c-7d7d-4e53-8aa6-bcec1553db3f" />
1 parent 1525dff commit e5aa941

File tree

10 files changed

+485
-62
lines changed

10 files changed

+485
-62
lines changed

crates/bevy_solari/src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ use bevy_render::settings::WgpuFeatures;
2626
/// An experimental set of plugins for raytraced lighting.
2727
///
2828
/// This plugin group provides:
29-
/// * [`SolariLightingPlugin`] - Raytraced direct and indirect lighting (indirect lighting not yet implemented).
29+
/// * [`SolariLightingPlugin`] - Raytraced direct and indirect lighting.
3030
/// * [`RaytracingScenePlugin`] - BLAS building, resource and lighting binding.
31+
///
32+
/// There's also:
3133
/// * [`pathtracer::PathtracingPlugin`] - A non-realtime pathtracer for validation purposes (not added by default).
3234
///
33-
/// To get started, add `RaytracingMesh3d` and `MeshMaterial3d::<StandardMaterial>` to your entities.
35+
/// To get started, add this plugin to your app, and then add `RaytracingMesh3d` and `MeshMaterial3d::<StandardMaterial>` to your entities.
3436
pub struct SolariPlugins;
3537

3638
impl PluginGroup for SolariPlugins {

crates/bevy_solari/src/pathtracer/pathtracer.wgsl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3<u32>) {
4747
if ray_t_min == 0.0 { radiance = ray_hit.material.emissive; }
4848

4949
// Sample direct lighting
50-
radiance += throughput * diffuse_brdf * sample_random_light(ray_hit.world_position, ray_hit.world_normal, &rng);
50+
let direct_lighting = sample_random_light(ray_hit.world_position, ray_hit.world_normal, &rng);
51+
radiance += throughput * diffuse_brdf * direct_lighting.radiance * direct_lighting.inverse_pdf;
5152

5253
// Sample new ray direction from the material BRDF for next bounce
5354
ray_direction = sample_cosine_hemisphere(ray_hit.world_normal, &rng);

crates/bevy_solari/src/realtime/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@ use node::SolariLightingNode;
2323
use prepare::prepare_solari_lighting_resources;
2424
use tracing::warn;
2525

26+
/// Raytraced direct and indirect lighting.
27+
///
28+
/// When using this plugin, it's highly recommended to set `shadows_enabled: false` on all lights, as Solari replaces
29+
/// traditional shadow mapping.
2630
pub struct SolariLightingPlugin;
2731

2832
impl Plugin for SolariLightingPlugin {
2933
fn build(&self, app: &mut App) {
3034
embedded_asset!(app, "restir_di.wgsl");
35+
embedded_asset!(app, "restir_gi.wgsl");
3136

3237
app.register_type::<SolariLighting>()
3338
.insert_resource(DefaultOpaqueRendererMethod::deferred());

crates/bevy_solari/src/realtime/node.rs

Lines changed: 77 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ pub mod graph {
3636

3737
pub struct SolariLightingNode {
3838
bind_group_layout: BindGroupLayout,
39-
initial_and_temporal_pipeline: CachedComputePipelineId,
40-
spatial_and_shade_pipeline: CachedComputePipelineId,
39+
di_initial_and_temporal_pipeline: CachedComputePipelineId,
40+
di_spatial_and_shade_pipeline: CachedComputePipelineId,
41+
gi_initial_and_temporal_pipeline: CachedComputePipelineId,
42+
gi_spatial_and_shade_pipeline: CachedComputePipelineId,
4143
}
4244

4345
impl ViewNode for SolariLightingNode {
@@ -72,8 +74,10 @@ impl ViewNode for SolariLightingNode {
7274
let previous_view_uniforms = world.resource::<PreviousViewUniforms>();
7375
let frame_count = world.resource::<FrameCount>();
7476
let (
75-
Some(initial_and_temporal_pipeline),
76-
Some(spatial_and_shade_pipeline),
77+
Some(di_initial_and_temporal_pipeline),
78+
Some(di_spatial_and_shade_pipeline),
79+
Some(gi_initial_and_temporal_pipeline),
80+
Some(gi_spatial_and_shade_pipeline),
7781
Some(scene_bindings),
7882
Some(viewport),
7983
Some(gbuffer),
@@ -82,8 +86,10 @@ impl ViewNode for SolariLightingNode {
8286
Some(view_uniforms),
8387
Some(previous_view_uniforms),
8488
) = (
85-
pipeline_cache.get_compute_pipeline(self.initial_and_temporal_pipeline),
86-
pipeline_cache.get_compute_pipeline(self.spatial_and_shade_pipeline),
89+
pipeline_cache.get_compute_pipeline(self.di_initial_and_temporal_pipeline),
90+
pipeline_cache.get_compute_pipeline(self.di_spatial_and_shade_pipeline),
91+
pipeline_cache.get_compute_pipeline(self.gi_initial_and_temporal_pipeline),
92+
pipeline_cache.get_compute_pipeline(self.gi_spatial_and_shade_pipeline),
8793
&scene_bindings.bind_group,
8894
camera.physical_viewport_size,
8995
view_prepass_textures.deferred_view(),
@@ -101,8 +107,18 @@ impl ViewNode for SolariLightingNode {
101107
&self.bind_group_layout,
102108
&BindGroupEntries::sequential((
103109
view_target.get_unsampled_color_attachment().view,
104-
solari_lighting_resources.reservoirs_a.as_entire_binding(),
105-
solari_lighting_resources.reservoirs_b.as_entire_binding(),
110+
solari_lighting_resources
111+
.di_reservoirs_a
112+
.as_entire_binding(),
113+
solari_lighting_resources
114+
.di_reservoirs_b
115+
.as_entire_binding(),
116+
solari_lighting_resources
117+
.gi_reservoirs_a
118+
.as_entire_binding(),
119+
solari_lighting_resources
120+
.gi_reservoirs_b
121+
.as_entire_binding(),
106122
gbuffer,
107123
depth_buffer,
108124
motion_vectors,
@@ -135,14 +151,20 @@ impl ViewNode for SolariLightingNode {
135151
],
136152
);
137153

138-
pass.set_pipeline(initial_and_temporal_pipeline);
154+
pass.set_pipeline(di_initial_and_temporal_pipeline);
139155
pass.set_push_constants(
140156
0,
141157
bytemuck::cast_slice(&[frame_index, solari_lighting.reset as u32]),
142158
);
143159
pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1);
144160

145-
pass.set_pipeline(spatial_and_shade_pipeline);
161+
pass.set_pipeline(di_spatial_and_shade_pipeline);
162+
pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1);
163+
164+
pass.set_pipeline(gi_initial_and_temporal_pipeline);
165+
pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1);
166+
167+
pass.set_pipeline(gi_spatial_and_shade_pipeline);
146168
pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1);
147169

148170
pass_span.end(&mut pass);
@@ -189,10 +211,12 @@ impl FromWorld for SolariLightingNode {
189211
(
190212
texture_storage_2d(
191213
ViewTarget::TEXTURE_FORMAT_HDR,
192-
StorageTextureAccess::WriteOnly,
214+
StorageTextureAccess::ReadWrite,
193215
),
194216
storage_buffer_sized(false, None),
195217
storage_buffer_sized(false, None),
218+
storage_buffer_sized(false, None),
219+
storage_buffer_sized(false, None),
196220
texture_2d(TextureSampleType::Uint),
197221
texture_depth_2d(),
198222
texture_2d(TextureSampleType::Float { filterable: true }),
@@ -204,9 +228,9 @@ impl FromWorld for SolariLightingNode {
204228
),
205229
);
206230

207-
let initial_and_temporal_pipeline =
231+
let di_initial_and_temporal_pipeline =
208232
pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
209-
label: Some("solari_lighting_initial_and_temporal_pipeline".into()),
233+
label: Some("solari_lighting_di_initial_and_temporal_pipeline".into()),
210234
layout: vec![
211235
scene_bindings.bind_group_layout.clone(),
212236
bind_group_layout.clone(),
@@ -220,9 +244,9 @@ impl FromWorld for SolariLightingNode {
220244
..default()
221245
});
222246

223-
let spatial_and_shade_pipeline =
247+
let di_spatial_and_shade_pipeline =
224248
pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
225-
label: Some("solari_lighting_spatial_and_shade_pipeline".into()),
249+
label: Some("solari_lighting_di_spatial_and_shade_pipeline".into()),
226250
layout: vec![
227251
scene_bindings.bind_group_layout.clone(),
228252
bind_group_layout.clone(),
@@ -236,10 +260,46 @@ impl FromWorld for SolariLightingNode {
236260
..default()
237261
});
238262

263+
let gi_initial_and_temporal_pipeline =
264+
pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
265+
label: Some("solari_lighting_gi_initial_and_temporal_pipeline".into()),
266+
layout: vec![
267+
scene_bindings.bind_group_layout.clone(),
268+
bind_group_layout.clone(),
269+
],
270+
push_constant_ranges: vec![PushConstantRange {
271+
stages: ShaderStages::COMPUTE,
272+
range: 0..8,
273+
}],
274+
shader: load_embedded_asset!(world, "restir_gi.wgsl"),
275+
shader_defs: vec![],
276+
entry_point: Some("initial_and_temporal".into()),
277+
zero_initialize_workgroup_memory: false,
278+
});
279+
280+
let gi_spatial_and_shade_pipeline =
281+
pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
282+
label: Some("solari_lighting_gi_spatial_and_shade_pipeline".into()),
283+
layout: vec![
284+
scene_bindings.bind_group_layout.clone(),
285+
bind_group_layout.clone(),
286+
],
287+
push_constant_ranges: vec![PushConstantRange {
288+
stages: ShaderStages::COMPUTE,
289+
range: 0..8,
290+
}],
291+
shader: load_embedded_asset!(world, "restir_gi.wgsl"),
292+
shader_defs: vec![],
293+
entry_point: Some("spatial_and_shade".into()),
294+
zero_initialize_workgroup_memory: false,
295+
});
296+
239297
Self {
240298
bind_group_layout,
241-
initial_and_temporal_pipeline,
242-
spatial_and_shade_pipeline,
299+
di_initial_and_temporal_pipeline,
300+
di_spatial_and_shade_pipeline,
301+
gi_initial_and_temporal_pipeline,
302+
gi_spatial_and_shade_pipeline,
243303
}
244304
}
245305
}

crates/bevy_solari/src/realtime/prepare.rs

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,19 @@ use bevy_render::{
1717
renderer::RenderDevice,
1818
};
1919

20-
/// Size of a Reservoir shader struct in bytes.
21-
const RESERVOIR_STRUCT_SIZE: u64 = 32;
20+
/// Size of a DI Reservoir shader struct in bytes.
21+
const DI_RESERVOIR_STRUCT_SIZE: u64 = 32;
22+
23+
/// Size of a GI Reservoir shader struct in bytes.
24+
const GI_RESERVOIR_STRUCT_SIZE: u64 = 48;
2225

2326
/// Internal rendering resources used for Solari lighting.
2427
#[derive(Component)]
2528
pub struct SolariLightingResources {
26-
pub reservoirs_a: Buffer,
27-
pub reservoirs_b: Buffer,
29+
pub di_reservoirs_a: Buffer,
30+
pub di_reservoirs_b: Buffer,
31+
pub gi_reservoirs_a: Buffer,
32+
pub gi_reservoirs_b: Buffer,
2833
pub previous_gbuffer: (Texture, TextureView),
2934
pub previous_depth: (Texture, TextureView),
3035
pub view_size: UVec2,
@@ -47,18 +52,30 @@ pub fn prepare_solari_lighting_resources(
4752
continue;
4853
}
4954

50-
let size = (view_size.x * view_size.y) as u64 * RESERVOIR_STRUCT_SIZE;
55+
let di_reservoirs_a = render_device.create_buffer(&BufferDescriptor {
56+
label: Some("solari_lighting_di_reservoirs_a"),
57+
size: (view_size.x * view_size.y) as u64 * DI_RESERVOIR_STRUCT_SIZE,
58+
usage: BufferUsages::STORAGE,
59+
mapped_at_creation: false,
60+
});
61+
62+
let di_reservoirs_b = render_device.create_buffer(&BufferDescriptor {
63+
label: Some("solari_lighting_di_reservoirs_b"),
64+
size: (view_size.x * view_size.y) as u64 * DI_RESERVOIR_STRUCT_SIZE,
65+
usage: BufferUsages::STORAGE,
66+
mapped_at_creation: false,
67+
});
5168

52-
let reservoirs_a = render_device.create_buffer(&BufferDescriptor {
53-
label: Some("solari_lighting_reservoirs_a"),
54-
size,
69+
let gi_reservoirs_a = render_device.create_buffer(&BufferDescriptor {
70+
label: Some("solari_lighting_gi_reservoirs_a"),
71+
size: (view_size.x * view_size.y) as u64 * GI_RESERVOIR_STRUCT_SIZE,
5572
usage: BufferUsages::STORAGE,
5673
mapped_at_creation: false,
5774
});
5875

59-
let reservoirs_b = render_device.create_buffer(&BufferDescriptor {
60-
label: Some("solari_lighting_reservoirs_b"),
61-
size,
76+
let gi_reservoirs_b = render_device.create_buffer(&BufferDescriptor {
77+
label: Some("solari_lighting_gi_reservoirs_b"),
78+
size: (view_size.x * view_size.y) as u64 * GI_RESERVOIR_STRUCT_SIZE,
6279
usage: BufferUsages::STORAGE,
6380
mapped_at_creation: false,
6481
});
@@ -88,8 +105,10 @@ pub fn prepare_solari_lighting_resources(
88105
let previous_depth_view = previous_depth.create_view(&TextureViewDescriptor::default());
89106

90107
commands.entity(entity).insert(SolariLightingResources {
91-
reservoirs_a,
92-
reservoirs_b,
108+
di_reservoirs_a,
109+
di_reservoirs_b,
110+
gi_reservoirs_a,
111+
gi_reservoirs_b,
93112
previous_gbuffer: (previous_gbuffer, previous_gbuffer_view),
94113
previous_depth: (previous_depth, previous_depth_view),
95114
view_size,

0 commit comments

Comments
 (0)