Skip to content

Commit c6bc114

Browse files
JMS55SparkyPotato
andauthored
Solari indirect specular support (#21355)
# Objective - Add initial support for indirect specular lighting (reflections) to Solari's realtime renderer ## Solution - A new pass is added called `specular_gi` - For rough materials, we reuse the path from [diffuse] ReSTIR GI, and just shade using the specular BRDF - For smooth materials, we trace a new path with up to 3 bounces, terminating in the world cache once we've hit a rough enough surface - DLSS-RR guide buffers now use correct diffuse and specular albedos ## Future * Specular motion vectors for DLSS-RR guiding are not yet implemented. I think I want to leave this for a future PR. For now they're hardcoded to zero. * Reflections can reveal the world cache * General quality is not great, there's probably some heuristics we can tune to reduce the noise, but at least it's a baseline to start with. ## Showcase Before <img width="2564" height="1500" alt="image" src="https://github.com/user-attachments/assets/07839d09-4692-40ff-8b25-f7e1e9787a7e" /> After <img width="2564" height="1500" alt="image" src="https://github.com/user-attachments/assets/79772fbc-dca5-4170-bed7-73fe0e3e182a" /> --------- Co-authored-by: SparkyPotato <[email protected]>
1 parent 409857f commit c6bc114

File tree

4 files changed

+175
-10
lines changed

4 files changed

+175
-10
lines changed

crates/bevy_solari/src/realtime/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ impl Plugin for SolariLightingPlugin {
3636
load_shader_library!(app, "presample_light_tiles.wgsl");
3737
embedded_asset!(app, "restir_di.wgsl");
3838
embedded_asset!(app, "restir_gi.wgsl");
39+
embedded_asset!(app, "specular_gi.wgsl");
3940
load_shader_library!(app, "world_cache_query.wgsl");
4041
embedded_asset!(app, "world_cache_compact.wgsl");
4142
embedded_asset!(app, "world_cache_update.wgsl");

crates/bevy_solari/src/realtime/node.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pub struct SolariLightingNode {
5555
di_spatial_and_shade_pipeline: CachedComputePipelineId,
5656
gi_initial_and_temporal_pipeline: CachedComputePipelineId,
5757
gi_spatial_and_shade_pipeline: CachedComputePipelineId,
58+
specular_gi_pipeline: CachedComputePipelineId,
5859
#[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
5960
resolve_dlss_rr_textures_pipeline: CachedComputePipelineId,
6061
}
@@ -120,6 +121,7 @@ impl ViewNode for SolariLightingNode {
120121
Some(di_spatial_and_shade_pipeline),
121122
Some(gi_initial_and_temporal_pipeline),
122123
Some(gi_spatial_and_shade_pipeline),
124+
Some(specular_gi_pipeline),
123125
Some(scene_bindings),
124126
Some(gbuffer),
125127
Some(depth_buffer),
@@ -139,6 +141,7 @@ impl ViewNode for SolariLightingNode {
139141
pipeline_cache.get_compute_pipeline(self.di_spatial_and_shade_pipeline),
140142
pipeline_cache.get_compute_pipeline(self.gi_initial_and_temporal_pipeline),
141143
pipeline_cache.get_compute_pipeline(self.gi_spatial_and_shade_pipeline),
144+
pipeline_cache.get_compute_pipeline(self.specular_gi_pipeline),
142145
&scene_bindings.bind_group,
143146
view_prepass_textures.deferred_view(),
144147
view_prepass_textures.depth_view(),
@@ -318,6 +321,13 @@ impl ViewNode for SolariLightingNode {
318321
);
319322
pass.dispatch_workgroups(dx, dy, 1);
320323

324+
pass.set_pipeline(specular_gi_pipeline);
325+
pass.set_push_constants(
326+
0,
327+
bytemuck::cast_slice(&[frame_index, solari_lighting.reset as u32]),
328+
);
329+
pass.dispatch_workgroups(dx, dy, 1);
330+
321331
pass_span.end(&mut pass);
322332
drop(pass);
323333

@@ -530,6 +540,13 @@ impl FromWorld for SolariLightingNode {
530540
None,
531541
vec![],
532542
),
543+
specular_gi_pipeline: create_pipeline(
544+
"solari_lighting_specular_gi_pipeline",
545+
"specular_gi",
546+
load_embedded_asset!(world, "specular_gi.wgsl"),
547+
None,
548+
vec![],
549+
),
533550
#[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
534551
resolve_dlss_rr_textures_pipeline: create_pipeline(
535552
"solari_lighting_resolve_dlss_rr_textures_pipeline",
Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
#import bevy_pbr::pbr_deferred_types::unpack_24bit_normal
2-
#import bevy_pbr::utils::octahedral_decode
1+
#import bevy_pbr::pbr_functions::{calculate_diffuse_color, calculate_F0}
32
#import bevy_render::view::View
3+
#import bevy_solari::gbuffer_utils::gpixel_resolve
44

55
@group(1) @binding(7) var gbuffer: texture_2d<u32>;
6+
@group(1) @binding(8) var depth_buffer: texture_depth_2d;
67
@group(1) @binding(12) var<uniform> view: View;
78

89
@group(2) @binding(0) var diffuse_albedo: texture_storage_2d<rgba8unorm, write>;
@@ -15,14 +16,49 @@ fn resolve_dlss_rr_textures(@builtin(global_invocation_id) global_id: vec3<u32>)
1516
let pixel_id = global_id.xy;
1617
if any(pixel_id >= vec2u(view.main_pass_viewport.zw)) { return; }
1718

18-
let gpixel = textureLoad(gbuffer, pixel_id, 0);
19-
let base_rough = unpack4x8unorm(gpixel.r);
20-
let base_color = pow(base_rough.rgb, vec3(2.2));
21-
let world_normal = octahedral_decode(unpack_24bit_normal(gpixel.a));
22-
let perceptual_roughness = base_rough.a;
19+
let depth = textureLoad(depth_buffer, global_id.xy, 0);
20+
if depth == 0.0 {
21+
textureStore(diffuse_albedo, pixel_id, vec4(0.0));
22+
textureStore(specular_albedo, pixel_id, vec4(0.5));
23+
textureStore(normal_roughness, pixel_id, vec4(0.0));
24+
textureStore(specular_motion_vectors, pixel_id, vec4(0.0));
25+
return;
26+
}
2327

24-
textureStore(diffuse_albedo, pixel_id, vec4(base_color, 0.0));
25-
textureStore(specular_albedo, pixel_id, vec4(0.0)); // TODO
26-
textureStore(normal_roughness, pixel_id, vec4(world_normal, perceptual_roughness));
28+
let surface = gpixel_resolve(textureLoad(gbuffer, pixel_id, 0), depth, pixel_id, view.main_pass_viewport.zw, view.world_from_clip);
29+
let F0 = calculate_F0(surface.material.base_color, surface.material.metallic, surface.material.reflectance);
30+
let wo = normalize(view.world_position - surface.world_position);
31+
32+
textureStore(diffuse_albedo, pixel_id, vec4(calculate_diffuse_color(surface.material.base_color, surface.material.metallic, 0.0, 0.0), 0.0));
33+
textureStore(specular_albedo, pixel_id, vec4(env_brdf_approx2(F0, surface.material.roughness, surface.world_normal, wo), 0.0));
34+
textureStore(normal_roughness, pixel_id, vec4(surface.world_normal, surface.material.perceptual_roughness));
2735
textureStore(specular_motion_vectors, pixel_id, vec4(0.0)); // TODO
2836
}
37+
38+
fn env_brdf_approx2(specular_color: vec3<f32>, alpha: f32, N: vec3<f32>, V: vec3<f32>) -> vec3<f32> {
39+
let NoV = abs(dot(N, V));
40+
41+
var X: vec4<f32>;
42+
X.x = 1.0;
43+
X.y = NoV;
44+
X.z = NoV * NoV;
45+
X.w = NoV * X.z;
46+
47+
var Y: vec4<f32>;
48+
Y.x = 1.0;
49+
Y.y = alpha;
50+
Y.z = alpha * alpha;
51+
Y.w = alpha * Y.z;
52+
53+
let M1 = mat2x2<f32>(0.99044, 1.29678, -1.28514, -0.755907);
54+
let M2 = mat3x3<f32>(1.0, 20.3225, 121.563, 2.92338, -27.0302, 626.13, 59.4188, 222.592, 316.627);
55+
let M3 = mat2x2<f32>(0.0365463, 9.0632, 3.32707, -9.04756);
56+
let M4 = mat3x3<f32>(1.0, 9.04401, 5.56589, 3.59685, -16.3174, 19.7886, -1.36772, 9.22949, -20.2123);
57+
58+
var bias = dot(M1 * X.xy, Y.xy) / dot(M2 * X.xyw, Y.xyw);
59+
let scale = dot(M3 * X.xy, Y.xy) / dot(M4 * X.xzw, Y.xyw);
60+
61+
bias *= saturate(specular_color.g * 50.0);
62+
63+
return fma(specular_color, vec3(max(0.0, scale)), vec3(max(0.0, bias)));
64+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#import bevy_pbr::pbr_functions::calculate_tbn_mikktspace
2+
#import bevy_render::maths::{orthonormalize, PI}
3+
#import bevy_render::view::View
4+
#import bevy_solari::brdf::{evaluate_brdf, evaluate_specular_brdf}
5+
#import bevy_solari::gbuffer_utils::gpixel_resolve
6+
#import bevy_solari::sampling::{sample_ggx_vndf, ggx_vndf_pdf}
7+
#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN, RAY_T_MAX}
8+
#import bevy_solari::world_cache::query_world_cache
9+
10+
@group(1) @binding(0) var view_output: texture_storage_2d<rgba16float, read_write>;
11+
@group(1) @binding(5) var<storage, read_write> gi_reservoirs_a: array<Reservoir>;
12+
@group(1) @binding(7) var gbuffer: texture_2d<u32>;
13+
@group(1) @binding(8) var depth_buffer: texture_depth_2d;
14+
@group(1) @binding(12) var<uniform> view: View;
15+
struct PushConstants { frame_index: u32, reset: u32 }
16+
var<push_constant> constants: PushConstants;
17+
18+
@compute @workgroup_size(8, 8, 1)
19+
fn specular_gi(@builtin(global_invocation_id) global_id: vec3<u32>) {
20+
if any(global_id.xy >= vec2u(view.main_pass_viewport.zw)) { return; }
21+
22+
let pixel_index = global_id.x + global_id.y * u32(view.main_pass_viewport.z);
23+
var rng = pixel_index + constants.frame_index;
24+
25+
let depth = textureLoad(depth_buffer, global_id.xy, 0);
26+
if depth == 0.0 {
27+
return;
28+
}
29+
let surface = gpixel_resolve(textureLoad(gbuffer, global_id.xy, 0), depth, global_id.xy, view.main_pass_viewport.zw, view.world_from_clip);
30+
31+
let wo = normalize(view.world_position - surface.world_position);
32+
33+
var radiance: vec3<f32>;
34+
var wi: vec3<f32>;
35+
if surface.material.roughness > 0.04 {
36+
// Surface is very rough, reuse the ReSTIR GI reservoir
37+
let gi_reservoir = gi_reservoirs_a[pixel_index];
38+
wi = normalize(gi_reservoir.sample_point_world_position - surface.world_position);
39+
radiance = gi_reservoir.radiance * gi_reservoir.unbiased_contribution_weight;
40+
} else {
41+
// Surface is glossy or mirror-like, trace a new path
42+
let TBN = orthonormalize(surface.world_normal);
43+
let T = TBN[0];
44+
let B = TBN[1];
45+
let N = TBN[2];
46+
let wo_tangent = vec3(dot(wo, T), dot(wo, B), dot(wo, N));
47+
let wi_tangent = sample_ggx_vndf(wo_tangent, surface.material.roughness, &rng);
48+
wi = wi_tangent.x * T + wi_tangent.y * B + wi_tangent.z * N;
49+
let pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, surface.material.roughness);
50+
51+
radiance = trace_glossy_path(surface.world_position, wi, &rng) / pdf;
52+
}
53+
54+
let brdf = evaluate_specular_brdf(surface.world_normal, wo, wi, surface.material.base_color, surface.material.metallic,
55+
surface.material.reflectance, surface.material.perceptual_roughness, surface.material.roughness);
56+
let cos_theta = saturate(dot(wi, surface.world_normal));
57+
radiance *= brdf * cos_theta * view.exposure;
58+
59+
var pixel_color = textureLoad(view_output, global_id.xy);
60+
pixel_color += vec4(radiance, 0.0);
61+
textureStore(view_output, global_id.xy, pixel_color);
62+
}
63+
64+
fn trace_glossy_path(initial_ray_origin: vec3<f32>, initial_wi: vec3<f32>, rng: ptr<function, u32>) -> vec3<f32> {
65+
var ray_origin = initial_ray_origin;
66+
var wi = initial_wi;
67+
68+
// Trace up to three bounces, getting the net throughput from them
69+
var throughput = vec3(1.0);
70+
for (var i = 0u; i < 3u; i += 1u) {
71+
// Trace ray
72+
let ray = trace_ray(ray_origin, wi, RAY_T_MIN, RAY_T_MAX, RAY_FLAG_NONE);
73+
if ray.kind == RAY_QUERY_INTERSECTION_NONE { break; }
74+
let ray_hit = resolve_ray_hit_full(ray);
75+
76+
// Surface is very rough, terminate path in the world cache
77+
if ray_hit.material.roughness > 0.04 || i == 2u {
78+
let diffuse_brdf = ray_hit.material.base_color / PI;
79+
return throughput * diffuse_brdf * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position);
80+
}
81+
82+
// Sample new ray direction from the GGX BRDF for next bounce
83+
let TBN = calculate_tbn_mikktspace(ray_hit.world_normal, ray_hit.world_tangent);
84+
let T = TBN[0];
85+
let B = TBN[1];
86+
let N = TBN[2];
87+
let wo = -wi;
88+
let wo_tangent = vec3(dot(wo, T), dot(wo, B), dot(wo, N));
89+
let wi_tangent = sample_ggx_vndf(wo_tangent, ray_hit.material.roughness, rng);
90+
wi = wi_tangent.x * T + wi_tangent.y * B + wi_tangent.z * N;
91+
ray_origin = ray_hit.world_position;
92+
93+
// Update throughput for next bounce
94+
let pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, ray_hit.material.roughness);
95+
let brdf = evaluate_brdf(N, wo, wi, ray_hit.material);
96+
let cos_theta = dot(wi, N);
97+
throughput *= (brdf * cos_theta) / pdf;
98+
}
99+
100+
return vec3(0.0);
101+
}
102+
103+
// Don't adjust the size of this struct without also adjusting GI_RESERVOIR_STRUCT_SIZE.
104+
struct Reservoir {
105+
sample_point_world_position: vec3<f32>,
106+
weight_sum: f32,
107+
radiance: vec3<f32>,
108+
confidence_weight: f32,
109+
sample_point_world_normal: vec3<f32>,
110+
unbiased_contribution_weight: f32,
111+
}

0 commit comments

Comments
 (0)