1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
mod extract;
mod node;
mod prepare;
use crate::{
scene::{prepare_raytracing_scene_resources, RaytracingSceneBindings},
SolariPlugins,
};
use bevy_app::{App, Plugin, PostUpdate};
use bevy_asset::embedded_asset;
use bevy_camera::Hdr;
use bevy_core_pipeline::{
core_3d::main_opaque_pass_3d,
prepass::{
DeferredPrepass, DeferredPrepassDoubleBuffer, DepthPrepass, DepthPrepassDoubleBuffer,
MotionVectorPrepass,
},
schedule::{Core3d, Core3dSystems},
};
use bevy_ecs::{
component::Component,
entity::Entity,
query::Has,
reflect::ReflectComponent,
schedule::IntoScheduleConfigs,
system::{Commands, Query},
};
use bevy_pbr::DefaultOpaqueRendererMethod;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
init_gpu_resource, renderer::RenderDevice, ExtractSchedule, Render, RenderApp, RenderStartup,
RenderSystems,
};
use bevy_shader::load_shader_library;
use extract::extract_solari_lighting;
use node::{init_solari_lighting_pipelines, solari_lighting};
use prepare::{
prepare_solari_lighting_resources, setup_raytracing_scene_needs_previous_frame_data,
};
use tracing::warn;
/// Raytraced direct and indirect lighting.
///
/// When using this plugin, it's highly recommended to set `shadow_maps_enabled: false` on all lights, as Solari replaces
/// traditional shadow mapping.
pub struct SolariLightingPlugin;
impl Plugin for SolariLightingPlugin {
fn build(&self, app: &mut App) {
load_shader_library!(app, "gbuffer_utils.wesl");
load_shader_library!(app, "bindings.wesl");
load_shader_library!(app, "presample_light_tiles.wesl");
load_shader_library!(app, "initial_path.wesl");
embedded_asset!(app, "restir.wesl");
embedded_asset!(app, "no_restir.wesl");
load_shader_library!(app, "world_cache_query.wesl");
embedded_asset!(app, "world_cache_compact.wesl");
embedded_asset!(app, "world_cache_update.wesl");
load_shader_library!(app, "resolve_dlss_rr_textures.wesl");
app.insert_resource(DefaultOpaqueRendererMethod::deferred());
}
fn finish(&self, app: &mut App) {
let features = app
.sub_app(RenderApp)
.world()
.resource::<RenderDevice>()
.features();
if !features.contains(SolariPlugins::required_wgpu_features()) {
warn!(
"SolariLightingPlugin not loaded. GPU lacks support for required features: {:?}.",
SolariPlugins::required_wgpu_features().difference(features)
);
return;
}
app.add_systems(PostUpdate, manage_prepass_double_buffers);
app.sub_app_mut(RenderApp)
.add_systems(
RenderStartup,
init_solari_lighting_pipelines.after(init_gpu_resource::<RaytracingSceneBindings>),
)
.add_systems(ExtractSchedule, extract_solari_lighting)
.add_systems(
Render,
(
prepare_solari_lighting_resources,
setup_raytracing_scene_needs_previous_frame_data
.before(prepare_raytracing_scene_resources),
)
.in_set(RenderSystems::PrepareResources),
)
.add_systems(
Core3d,
solari_lighting
.before(main_opaque_pass_3d)
.in_set(Core3dSystems::MainPass),
);
}
}
/// A component for a 3d camera entity to enable the Solari raytraced lighting system.
///
/// Must be used with `CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING)`, and
/// `Msaa::Off`.
#[derive(Component, Reflect, Clone)]
#[reflect(Component, Default, Clone)]
#[require(Hdr, DeferredPrepass, DepthPrepass, MotionVectorPrepass)]
pub struct SolariLighting {
/// [ReSTIR](https://en.wikipedia.org/wiki/Spatiotemporal_reservoir_resampling) is a technique to reuse path samples
/// between pixels and frames. This dramatically reduces noise, at the cost of a few extra rays per pixel.
///
/// However, modern denoisers cope well with very noisy input. In many cases, turning this on
/// won't dramatically improve quality after denoising.
///
/// If you want more fine shadow detail, or have scenes with more difficult lighting conditions,
/// turning this on may improve quality and stability, at the cost of a decent chunk of performance.
///
/// Whether to enable this setting or not will be very scene dependent.
///
/// Defaults to `false`.
pub restir: bool,
/// Maximum confidence weight (effective temporal history length) a pixel
/// can accumulate during temporal resampling.
///
/// Has no effect when [`SolariLighting::restir`] is `false`.
///
/// Higher values are more stable but slower to react to lighting changes
/// and will lead to increased artifacts.
pub confidence_weight_cap: f32,
/// Number of direct light samples taken for the camera's primary hit during
/// initial sampling.
///
/// Higher values reduce noise in directly-lit areas at the cost of more work
/// per frame. Lower values are faster but noisier.
pub primary_di_samples: u32,
/// Number of direct light samples taken at each indirect bounce during
/// initial sampling.
///
/// Higher values reduce noise in indirect lighting at the cost of more work
/// per frame. Lower values are faster but noisier.
pub secondary_di_samples: u32,
/// Maximum number of bounces traced when generating an initial path.
///
/// Higher values capture more detail in nested reflections and more indirect lighting,
/// at the cost of more rays traced per frame. Lower values are faster but lose
/// multi-bounce lighting for specular paths.
pub max_bounces: u32,
/// How responsive the world cache is to changes in lighting.
///
/// Higher values accumulate more temporal history, giving more stable but
/// less responsive (slower to update) lighting. Lower values react faster
/// but are noisier and less stable.
pub world_cache_max_temporal_samples: f32,
/// How many direct light samples each world cache cell takes when updating
/// each frame.
///
/// Higher values reduce noise in cached lighting at the cost of more work
/// per frame. Lower values are faster but noisier.
pub world_cache_direct_light_sample_count: u32,
/// Maximum distance to trace GI rays between two world cache cells.
///
/// Higher values capture indirect light from farther away for more accurate
/// GI at the cost of longer (more expensive) ray traversal and increased noise.
/// Lower values are faster and less noisy but may miss distant lighting or leak sky lighting.
///
/// Rays that miss within this distance are treated as reaching the environment
/// map light, and leak sky lighting into the cache.
pub world_cache_max_gi_ray_distance: f32,
/// Soft upper limit on the number of world cache cells to update each frame.
///
/// Higher values let the cache converge faster after lighting changes at the
/// cost of more work per frame. Lower values are cheaper but make the cache
/// slower to update.
///
/// This is a stochastic target that only takes effect when the number of
/// active cells exceeds it: each active cell is then updated with
/// probability `target / active_cells`, so on average this many cells
/// update, though individual frames may update more or fewer. When there
/// are fewer active cells than the target, all of them update every frame.
pub world_cache_cell_updates_soft_target: u32,
/// Size of a world cache cell at the lowest LOD, in meters.
///
/// Smaller values give finer spatial resolution and more detailed indirect
/// lighting at the cost of more cells to fill and update. Larger values are
/// cheaper but coarser, which can cause light leaking.
pub world_cache_position_base_cell_size: f32,
/// How fast the world cache transitions between LODs as a function of
/// distance to the camera.
///
/// Higher values keep cells small (high detail) out to greater distances for
/// better quality at the cost of more cells to fill. Lower values transition
/// to larger cells sooner, which is cheaper but coarser farther from the
/// camera.
pub world_cache_position_lod_scale: f32,
/// Set to true to delete the saved temporal history (past frames).
///
/// Useful for preventing ghosting when the history is no longer
/// representative of the current frame, such as in sudden camera cuts.
///
/// After setting this to true, it will automatically be toggled
/// back to false at the end of the frame.
pub reset: bool,
}
impl Default for SolariLighting {
fn default() -> Self {
Self {
restir: false,
confidence_weight_cap: 8.0,
primary_di_samples: 8,
secondary_di_samples: 4,
max_bounces: 3,
world_cache_max_temporal_samples: 32.0,
world_cache_direct_light_sample_count: 32,
world_cache_max_gi_ray_distance: 50.0,
world_cache_cell_updates_soft_target: 40000,
world_cache_position_base_cell_size: 0.15,
world_cache_position_lod_scale: 15.0,
reset: true, // No temporal history on the first frame
}
}
}
/// Adds or removes the prepass double-buffer components according to [`SolariLighting::restir`].
fn manage_prepass_double_buffers(
views: Query<(
Entity,
&SolariLighting,
Has<DeferredPrepassDoubleBuffer>,
Has<DepthPrepassDoubleBuffer>,
)>,
mut commands: Commands,
) {
for (entity, solari_lighting, deferred_double_buffered, depth_double_buffered) in &views {
let mut entity = commands.entity(entity);
if solari_lighting.restir {
if !deferred_double_buffered {
entity.insert(DeferredPrepassDoubleBuffer);
}
if !depth_double_buffered {
entity.insert(DepthPrepassDoubleBuffer);
}
} else {
if deferred_double_buffered {
entity.remove::<DeferredPrepassDoubleBuffer>();
}
if depth_double_buffered {
entity.remove::<DepthPrepassDoubleBuffer>();
}
}
}
}