Skip to content

bevy_solari: RIS for Direct Lighting #19620

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1278,7 +1278,7 @@ required-features = ["bevy_solari"]

[package.metadata.example.solari]
name = "Solari"
description = "Demonstrates realtime dynamic global illumination rendering using Bevy Solari."
description = "Demonstrates realtime dynamic raytraced lighting using Bevy Solari."
category = "3D Rendering"
wasm = false

Expand Down
4 changes: 3 additions & 1 deletion crates/bevy_solari/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.16.0-dev" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" }
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.16.0-dev" }
bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" }
Expand All @@ -27,8 +28,9 @@ bevy_render = { path = "../bevy_render", version = "0.16.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" }

# other
tracing = { version = "0.1", default-features = false, features = ["std"] }
bytemuck = { version = "1" }
derive_more = { version = "1", default-features = false, features = ["from"] }
tracing = { version = "0.1", default-features = false, features = ["std"] }

[lints]
workspace = true
Expand Down
13 changes: 7 additions & 6 deletions crates/bevy_solari/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,36 @@
//!
//! ![`bevy_solari` logo](https://raw.githubusercontent.com/bevyengine/bevy/assets/branding/bevy_solari.svg)
pub mod pathtracer;
pub mod realtime;
pub mod scene;

/// The solari prelude.
///
/// This includes the most common types in this crate, re-exported for your convenience.
pub mod prelude {
pub use super::SolariPlugin;
pub use crate::pathtracer::Pathtracer;
pub use crate::realtime::SolariLighting;
pub use crate::scene::RaytracingMesh3d;
}

use crate::realtime::SolariLightingPlugin;
use crate::scene::RaytracingScenePlugin;
use bevy_app::{App, Plugin};
use bevy_render::settings::WgpuFeatures;
use pathtracer::PathtracingPlugin;
use scene::RaytracingScenePlugin;

/// An experimental plugin for raytraced lighting.
///
/// This plugin provides:
/// * (Coming soon) - Raytraced direct and indirect lighting.
/// * [`SolariLightingPlugin`] - Raytraced direct and indirect lighting (indirect lighting not yet implemented).
/// * [`RaytracingScenePlugin`] - BLAS building, resource and lighting binding.
/// * [`PathtracingPlugin`] - A non-realtime pathtracer for validation purposes.
/// * [`pathtracer::PathtracingPlugin`] - A non-realtime pathtracer for validation purposes.
///
/// To get started, add `RaytracingMesh3d` and `MeshMaterial3d::<StandardMaterial>` to your entities.
pub struct SolariPlugin;

impl Plugin for SolariPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((RaytracingScenePlugin, PathtracingPlugin));
app.add_plugins((RaytracingScenePlugin, SolariLightingPlugin));
}
}

Expand Down
107 changes: 107 additions & 0 deletions crates/bevy_solari/src/realtime/direct.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance
#import bevy_pbr::pbr_deferred_types::unpack_24bit_normal
#import bevy_pbr::rgb9e5::rgb9e5_to_vec3_
#import bevy_pbr::utils::{rand_f, octahedral_decode}
#import bevy_render::maths::PI
#import bevy_render::view::View
#import bevy_solari::reservoir::{Reservoir, empty_reservoir, reservoir_valid}
#import bevy_solari::sampling::{generate_random_light_sample, calculate_light_contribution, trace_light_visibility}

@group(1) @binding(0) var view_output: texture_storage_2d<rgba16float, write>;
@group(1) @binding(1) var<storage, read> previous_reservoirs: array<Reservoir>;
@group(1) @binding(2) var<storage, read_write> reservoirs: array<Reservoir>;
@group(1) @binding(3) var gbuffer: texture_2d<u32>;
@group(1) @binding(4) var depth_buffer: texture_depth_2d;
@group(1) @binding(5) var motion_vectors: texture_2d<f32>;
@group(1) @binding(6) var<uniform> view: View;
struct PushConstants { frame_index: u32, reset: u32 }
var<push_constant> constants: PushConstants;

const INITIAL_SAMPLES = 32u;
const SPATIAL_REUSE_RADIUS_PIXELS = 30.0;
const CONFIDENCE_WEIGHT_CAP = 20.0;

@compute @workgroup_size(8, 8, 1)
fn initial_samples(@builtin(global_invocation_id) global_id: vec3<u32>) {
if any(global_id.xy >= vec2u(view.viewport.zw)) { return; }

let pixel_index = global_id.x + global_id.y * u32(view.viewport.z);
var rng = pixel_index + constants.frame_index;

let depth = textureLoad(depth_buffer, global_id.xy, 0);
if depth == 0.0 {
reservoirs[pixel_index] = empty_reservoir();
return;
}
let gpixel = textureLoad(gbuffer, global_id.xy, 0);
let world_position = reconstruct_world_position(global_id.xy, depth);
let world_normal = octahedral_decode(unpack_24bit_normal(gpixel.a));
let base_color = pow(unpack4x8unorm(gpixel.r).rgb, vec3(2.2));
let diffuse_brdf = base_color / PI;

var reservoir = empty_reservoir();
var reservoir_target_function = 0.0;
for (var i = 0u; i < INITIAL_SAMPLES; i++) {
let light_sample = generate_random_light_sample(&rng);

let mis_weight = 1.0 / f32(INITIAL_SAMPLES);
let light_contribution = calculate_light_contribution(light_sample, world_position, world_normal);
let target_function = luminance(light_contribution.radiance * diffuse_brdf);
let resampling_weight = mis_weight * (target_function * light_contribution.inverse_pdf);

reservoir.weight_sum += resampling_weight;

if rand_f(&rng) < resampling_weight / reservoir.weight_sum {
reservoir.sample = light_sample;
reservoir_target_function = target_function;
}
}

if reservoir_valid(reservoir) {
let inverse_target_function = select(0.0, 1.0 / reservoir_target_function, reservoir_target_function > 0.0);
reservoir.unbiased_contribution_weight = reservoir.weight_sum * inverse_target_function;
reservoir.unbiased_contribution_weight *= trace_light_visibility(reservoir.sample, world_position);
}

reservoir.confidence_weight = 1.0;

reservoirs[pixel_index] = reservoir;
}

@compute @workgroup_size(8, 8, 1)
fn reuse_and_shade(@builtin(global_invocation_id) global_id: vec3<u32>) {
if any(global_id.xy >= vec2u(view.viewport.zw)) { return; }

let pixel_index = global_id.x + global_id.y * u32(view.viewport.z);

let depth = textureLoad(depth_buffer, global_id.xy, 0);
if depth == 0.0 {
textureStore(view_output, global_id.xy, vec4(vec3(0.0), 1.0));
return;
}
let gpixel = textureLoad(gbuffer, global_id.xy, 0);
let world_position = reconstruct_world_position(global_id.xy, depth);
let world_normal = octahedral_decode(unpack_24bit_normal(gpixel.a));
let base_color = pow(unpack4x8unorm(gpixel.r).rgb, vec3(2.2));
let diffuse_brdf = base_color / PI;
let emissive = rgb9e5_to_vec3_(gpixel.g);

let canonical_reservoir = reservoirs[pixel_index];
var radiance = vec3(0.0);
if reservoir_valid(canonical_reservoir) {
radiance = calculate_light_contribution(canonical_reservoir.sample, world_position, world_normal).radiance;
}

var pixel_color = radiance * canonical_reservoir.unbiased_contribution_weight;
pixel_color *= view.exposure;
pixel_color *= diffuse_brdf;
pixel_color += emissive;
textureStore(view_output, global_id.xy, vec4(pixel_color, 1.0));
}

fn reconstruct_world_position(pixel_id: vec2<u32>, depth: f32) -> vec3<f32> {
let uv = (vec2<f32>(pixel_id) + 0.5) / view.viewport.zw;
let xy_ndc = (uv - vec2(0.5)) * vec2(2.0, -2.0);
let world_pos = view.world_from_clip * vec4(xy_ndc, depth, 1.0);
return world_pos.xyz / world_pos.w;
}
27 changes: 27 additions & 0 deletions crates/bevy_solari/src/realtime/extract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use super::{prepare::SolariLightingResources, SolariLighting};
use bevy_ecs::system::{Commands, ResMut};
use bevy_pbr::deferred::SkipDeferredLighting;
use bevy_render::{camera::Camera, sync_world::RenderEntity, MainWorld};

pub fn extract_solari_lighting(mut main_world: ResMut<MainWorld>, mut commands: Commands) {
let mut cameras_3d = main_world.query::<(RenderEntity, &Camera, Option<&mut SolariLighting>)>();

for (entity, camera, mut solari_lighting) in cameras_3d.iter_mut(&mut main_world) {
let mut entity_commands = commands
.get_entity(entity)
.expect("Camera entity wasn't synced.");
if solari_lighting.is_some() && camera.is_active {
entity_commands.insert((
solari_lighting.as_deref().unwrap().clone(),
SkipDeferredLighting,
));
solari_lighting.as_mut().unwrap().reset = false;
} else {
entity_commands.remove::<(
SolariLighting,
SolariLightingResources,
SkipDeferredLighting,
)>();
}
}
}
80 changes: 80 additions & 0 deletions crates/bevy_solari/src/realtime/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
mod extract;
mod node;
mod prepare;

use crate::SolariPlugin;
use bevy_app::{App, Plugin};
use bevy_asset::embedded_asset;
use bevy_core_pipeline::{
core_3d::graph::{Core3d, Node3d},
prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass},
};
use bevy_ecs::{component::Component, reflect::ReflectComponent, schedule::IntoScheduleConfigs};
use bevy_pbr::DefaultOpaqueRendererMethod;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
load_shader_library,
render_graph::{RenderGraphApp, ViewNodeRunner},
renderer::RenderDevice,
view::Hdr,
ExtractSchedule, Render, RenderApp, RenderSystems,
};
use extract::extract_solari_lighting;
use node::SolariLightingNode;
use prepare::prepare_solari_lighting_resources;
use tracing::warn;

pub struct SolariLightingPlugin;

impl Plugin for SolariLightingPlugin {
fn build(&self, app: &mut App) {
embedded_asset!(app, "direct.wgsl");
load_shader_library!(app, "reservoir.wgsl");

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

fn finish(&self, app: &mut App) {
let render_app = app.sub_app_mut(RenderApp);

let render_device = render_app.world().resource::<RenderDevice>();
let features = render_device.features();
if !features.contains(SolariPlugin::required_wgpu_features()) {
warn!(
"SolariLightingPlugin not loaded. GPU lacks support for required features: {:?}.",
SolariPlugin::required_wgpu_features().difference(features)
);
return;
}
render_app
.add_systems(ExtractSchedule, extract_solari_lighting)
.add_systems(
Render,
prepare_solari_lighting_resources.in_set(RenderSystems::PrepareResources),
)
.add_render_graph_node::<ViewNodeRunner<SolariLightingNode>>(
Core3d,
node::graph::SolariLightingNode,
)
.add_render_graph_edges(
Core3d,
(Node3d::EndMainPass, node::graph::SolariLightingNode),
);
}
}

#[derive(Component, Reflect, Clone)]
#[reflect(Component, Default, Clone)]
#[require(Hdr, DeferredPrepass, DepthPrepass, MotionVectorPrepass)]
pub struct SolariLighting {
pub reset: bool,
}

impl Default for SolariLighting {
fn default() -> Self {
Self {
reset: true, // No temporal history on the first frame
}
}
}
Loading