Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
90e02c7
Adjust specular_multiscatter to not take LightingInput
JMS55 Jul 9, 2025
c73405b
Fix SSAO specular occlusion roughness bug
JMS55 Jul 9, 2025
f340c77
Add specular material properties to solari bindings
JMS55 Jul 9, 2025
dfb3401
Fix inverted ray in path tracer (dosen't actually affect results)
JMS55 Jul 9, 2025
bf5ed11
WIP specular material
JMS55 Jul 9, 2025
d80b8ef
Merge commit 'f964ee1e3ae2fc28ab2e7586b581271fb973e913' into solari6-…
JMS55 Jul 17, 2025
0ecbd8f
Fix merge
JMS55 Jul 17, 2025
18b5d1f
The good parts
JMS55 Jul 18, 2025
c474114
Remove extraneous h parameter for D_GGX
JMS55 Jul 19, 2025
b143c10
Add VNDF sampling routines
JMS55 Jul 19, 2025
b4a2f77
WIP specular pathtracer (removed NEE for now)
JMS55 Jul 19, 2025
09f36c4
Diffuse/specular weights always sum to 1
JMS55 Jul 19, 2025
82519a5
Add world_tangent to ResolvedRayHitFull
JMS55 Jul 19, 2025
02841d7
Fix specular
JMS55 Jul 19, 2025
3ebb7d4
Allow smoother specular materials
JMS55 Jul 19, 2025
3df77e2
Remove uneeded saturates
JMS55 Jul 19, 2025
6f9b1b5
Add back NEE
JMS55 Jul 19, 2025
4714d4e
Merge commit '6354a950ee4d2a282ebf955cdd0b9524872571b1' into solari6-…
JMS55 Jul 20, 2025
92bc08b
Refactoring
JMS55 Jul 20, 2025
454a861
Paper links
JMS55 Jul 20, 2025
548f73f
Update release notes with PR number
JMS55 Jul 20, 2025
aa4c545
CI
JMS55 Jul 20, 2025
140baf2
Merge branch 'main' into solari6-perfopt-good
JMS55 Jul 20, 2025
eb84298
Small fix
JMS55 Jul 20, 2025
ca464f1
Merge branch 'solari6-perfopt-good' of https://github.com/JMS55/bevy …
JMS55 Jul 20, 2025
5c5f099
Add scene limit checks
JMS55 Jul 20, 2025
aefcddf
Fix error message
JMS55 Jul 20, 2025
b377f53
Merge commit 'aefcddf2087b941e13b69a2a410cd1d35524dc9d' into solari6-…
JMS55 Jul 21, 2025
7430d90
Adjust example
JMS55 Jul 21, 2025
be3142b
Merge commit 'f858c0d6e10cb7b73d69c3d4de0c98d89ed041c4' into solari6-…
JMS55 Jul 22, 2025
2f8a1a4
Adjust release notes
JMS55 Jul 22, 2025
65b9caf
Misc
JMS55 Jul 23, 2025
47a4d04
Misc
JMS55 Jul 23, 2025
ae3aa09
Merge commit '4b1b70d5011cfac9fd5be3aab76ee4b01300c863' into solari6-…
JMS55 Jul 23, 2025
e79d7ae
implement mis into pt
SparkyPotato Jul 24, 2025
9aea643
add to release notes
SparkyPotato Jul 24, 2025
70f8ad8
address feedback and special case specular
SparkyPotato Jul 24, 2025
fb0cc10
Merge pull request #34 from SparkyPotato/solari6-mis
JMS55 Jul 25, 2025
b1464ca
Merge branch 'main' into solari6-specular
JMS55 Jul 25, 2025
5761e40
fix lower exposure
SparkyPotato Jul 26, 2025
2b2b3c7
Merge pull request #35 from SparkyPotato/solari6-mis
JMS55 Jul 26, 2025
878c089
Merge commit '8398cab668e2f5b6248cd8323b7356dc7fb6d27b' into solari6-…
JMS55 Jul 29, 2025
eaf575c
Merge branch 'main' into solari6-specular
alice-i-cecile Jul 31, 2025
1832942
Merge commit 'f96eaa4a80b2a6be735184a316c176a918e0f364' into solari6-…
JMS55 Aug 1, 2025
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
104 changes: 88 additions & 16 deletions crates/bevy_solari/src/pathtracer/pathtracer.wgsl
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance
#import bevy_pbr::pbr_functions::calculate_tbn_mikktspace
#import bevy_pbr::utils::{rand_f, rand_vec2f, sample_cosine_hemisphere}
#import bevy_render::maths::PI
#import bevy_render::view::View
#import bevy_solari::sampling::sample_random_light
#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN, RAY_T_MAX}
#import bevy_solari::brdf::evaluate_brdf
#import bevy_solari::sampling::{sample_random_light, random_light_pdf, sample_ggx_vndf, ggx_vndf_pdf, balance_heuristic, power_heuristic}
#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, ResolvedRayHitFull, RAY_T_MIN, RAY_T_MAX}

@group(1) @binding(0) var accumulation_texture: texture_storage_2d<rgba32float, read_write>;
@group(1) @binding(1) var view_output: texture_storage_2d<rgba16float, write>;
Expand Down Expand Up @@ -35,32 +37,45 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3<u32>) {
// Path trace
var radiance = vec3(0.0);
var throughput = vec3(1.0);
var p_bounce = 0.0;
var bounce_was_perfect_reflection = true;
var previous_normal = vec3(0.0);
loop {
let ray_hit = trace_ray(ray_origin, ray_direction, ray_t_min, RAY_T_MAX, RAY_FLAG_NONE);
if ray_hit.kind != RAY_QUERY_INTERSECTION_NONE {
let ray_hit = resolve_ray_hit_full(ray_hit);
let wo = -ray_direction;

// Evaluate material BRDF
let diffuse_brdf = ray_hit.material.base_color / PI;
var mis_weight = 1.0;
if !bounce_was_perfect_reflection {
let p_light = random_light_pdf(ray_hit);
mis_weight = power_heuristic(p_bounce, p_light);
}
radiance += mis_weight * throughput * ray_hit.material.emissive;

// Use emissive only on the first ray (coming from the camera)
if ray_t_min == 0.0 { radiance = ray_hit.material.emissive; }

// Sample direct lighting
let direct_lighting = sample_random_light(ray_hit.world_position, ray_hit.world_normal, &rng);
radiance += throughput * diffuse_brdf * direct_lighting.radiance * direct_lighting.inverse_pdf;
// Sample direct lighting, but only if the surface is not mirror-like
let is_perfectly_specular = ray_hit.material.roughness < 0.0001 && ray_hit.material.metallic > 0.9999;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_perfectly_specular is defined in two places, maybe have a function?

if !is_perfectly_specular {
let direct_lighting = sample_random_light(ray_hit.world_position, ray_hit.world_normal, &rng);
let pdf_of_bounce = brdf_pdf(wo, direct_lighting.wi, ray_hit);
mis_weight = power_heuristic(1.0 / direct_lighting.inverse_pdf, pdf_of_bounce);
let direct_lighting_brdf = evaluate_brdf(ray_hit.world_normal, wo, direct_lighting.wi, ray_hit.material);
radiance += mis_weight * throughput * direct_lighting.radiance * direct_lighting.inverse_pdf * direct_lighting_brdf;
}

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

// Update other variables for next bounce
let next_bounce = importance_sample_next_bounce(wo, ray_hit, &rng);
ray_direction = next_bounce.wi;
ray_origin = ray_hit.world_position;
ray_t_min = RAY_T_MIN;
p_bounce = next_bounce.pdf;
bounce_was_perfect_reflection = next_bounce.perfectly_specular_bounce;
previous_normal = ray_hit.world_normal;

// Update throughput for next bounce
let cos_theta = dot(-ray_direction, ray_hit.world_normal);
let cosine_hemisphere_pdf = cos_theta / PI; // Weight for the next bounce because we importance sampled the diffuse BRDF for the next ray direction
throughput *= (diffuse_brdf * cos_theta) / cosine_hemisphere_pdf;
let brdf = evaluate_brdf(ray_hit.world_normal, wo, next_bounce.wi, ray_hit.material);
let cos_theta = dot(next_bounce.wi, ray_hit.world_normal);
throughput *= (brdf * cos_theta) / next_bounce.pdf;

// Russian roulette for early termination
let p = luminance(throughput);
Expand All @@ -77,3 +92,60 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3<u32>) {
textureStore(accumulation_texture, global_id.xy, vec4(new_color, old_color.a + 1.0));
textureStore(view_output, global_id.xy, vec4(new_color, 1.0));
}

struct NextBounce {
wi: vec3<f32>,
pdf: f32,
perfectly_specular_bounce: bool,
}

fn importance_sample_next_bounce(wo: vec3<f32>, ray_hit: ResolvedRayHitFull, rng: ptr<function, u32>) -> NextBounce {
let is_perfectly_specular = ray_hit.material.roughness < 0.0001 && ray_hit.material.metallic > 0.9999;
if is_perfectly_specular {
return NextBounce(reflect(-wo, ray_hit.world_normal), 1.0, true);
}
let diffuse_weight = mix(mix(0.4f, 0.9f, ray_hit.material.perceptual_roughness), 0.f, ray_hit.material.metallic);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whats the 0.4 and 0.9 about? and why the mix(mix(? isnt the outer mix really just a multiply by (1.0-metallic) since its a mix towards 0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just random numbers that @SparkyPotato found to work well iirc.

let specular_weight = 1.0 - diffuse_weight;

let TBN = calculate_tbn_mikktspace(ray_hit.world_normal, ray_hit.world_tangent);
let T = TBN[0];
let B = TBN[1];
let N = TBN[2];

let wo_tangent = vec3(dot(wo, T), dot(wo, B), dot(wo, N));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If TBN is a mat3<f32>, this is just TBN * wo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this is the code we use everywhere else in bevy 😅


var wi: vec3<f32>;
var wi_tangent: vec3<f32>;
let diffuse_selected = rand_f(rng) < diffuse_weight;
if diffuse_selected {
wi = sample_cosine_hemisphere(ray_hit.world_normal, rng);
wi_tangent = vec3(dot(wi, T), dot(wi, B), dot(wi, N));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

} else {
wi_tangent = sample_ggx_vndf(wo_tangent, ray_hit.material.roughness, rng);
wi = wi_tangent.x * T + wi_tangent.y * B + wi_tangent.z * N;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this is a mul with the transpose.

}

let diffuse_pdf = dot(wi, ray_hit.world_normal) / PI;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have wi_tangent here, can just use the Z component (I assume that's up) instead of the dot.

let specular_pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, ray_hit.material.roughness);
let pdf = (diffuse_weight * diffuse_pdf) + (specular_weight * specular_pdf);

return NextBounce(wi, pdf, false);
}

fn brdf_pdf(wo: vec3<f32>, wi: vec3<f32>, ray_hit: ResolvedRayHitFull) -> f32 {
let diffuse_weight = mix(mix(0.4f, 0.9f, ray_hit.material.roughness), 0.f, ray_hit.material.metallic);
let specular_weight = 1.0 - diffuse_weight;

let TBN = calculate_tbn_mikktspace(ray_hit.world_normal, ray_hit.world_tangent);
let T = TBN[0];
let B = TBN[1];
let N = TBN[2];

let wo_tangent = vec3(dot(wo, T), dot(wo, B), dot(wo, N));
let wi_tangent = vec3(dot(wi, T), dot(wi, B), dot(wi, N));

let diffuse_pdf = wi_tangent.z / PI;
let specular_pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, ray_hit.material.roughness);
let pdf = (diffuse_weight * diffuse_pdf) + (specular_weight * specular_pdf);
return pdf;
}
33 changes: 25 additions & 8 deletions crates/bevy_solari/src/scene/binder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,23 @@ pub fn prepare_raytracing_scene_bindings(
let Some(emissive_texture_id) = process_texture(&material.emissive_texture) else {
continue;
};
let Some(metallic_roughness_texture_id) =
process_texture(&material.metallic_roughness_texture)
else {
continue;
};

materials.get_mut().push(GpuMaterial {
base_color: material.base_color.to_linear(),
emissive: material.emissive,
base_color_texture_id,
normal_map_texture_id,
base_color_texture_id,
emissive_texture_id,
metallic_roughness_texture_id,

base_color: LinearRgba::from(material.base_color).to_vec3(),
perceptual_roughness: material.perceptual_roughness,
emissive: material.emissive.to_vec3(),
metallic: material.metallic,
reflectance: LinearRgba::from(material.specular_tint).to_vec3() * material.reflectance,
_padding: Default::default(),
});

Expand Down Expand Up @@ -180,11 +190,12 @@ pub fn prepare_raytracing_scene_bindings(
vertex_buffer_offset: vertex_slice.range.start,
index_buffer_id,
index_buffer_offset: index_slice.range.start,
triangle_count: (index_slice.range.len() / 3) as u32,
});

material_ids.get_mut().push(material_id);

if material.emissive != LinearRgba::BLACK {
if material.emissive != Vec3::ZERO {
light_sources
.get_mut()
.push(GpuLightSource::new_emissive_mesh_light(
Expand Down Expand Up @@ -342,16 +353,22 @@ struct GpuInstanceGeometryIds {
vertex_buffer_offset: u32,
index_buffer_id: u32,
index_buffer_offset: u32,
triangle_count: u32,
}

#[derive(ShaderType)]
struct GpuMaterial {
base_color: LinearRgba,
emissive: LinearRgba,
base_color_texture_id: u32,
normal_map_texture_id: u32,
base_color_texture_id: u32,
emissive_texture_id: u32,
_padding: u32,
metallic_roughness_texture_id: u32,

base_color: Vec3,
perceptual_roughness: f32,
emissive: Vec3,
metallic: f32,
reflectance: Vec3,
_padding: f32,
}

#[derive(ShaderType)]
Expand Down
56 changes: 56 additions & 0 deletions crates/bevy_solari/src/scene/brdf.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#define_import_path bevy_solari::brdf

#import bevy_pbr::lighting::{F_AB, D_GGX, V_SmithGGXCorrelated, fresnel, specular_multiscatter}
#import bevy_pbr::pbr_functions::{calculate_diffuse_color, calculate_F0}
#import bevy_render::maths::PI
#import bevy_solari::scene_bindings::ResolvedMaterial

fn evaluate_brdf(
world_normal: vec3<f32>,
wo: vec3<f32>,
wi: vec3<f32>,
material: ResolvedMaterial,
) -> vec3<f32> {
let diffuse_brdf = diffuse_brdf(material.base_color, material.metallic);
let specular_brdf = specular_brdf(
world_normal,
wo,
wi,
material.base_color,
material.metallic,
material.reflectance,
material.perceptual_roughness,
material.roughness,
);
return diffuse_brdf + specular_brdf;
}

fn diffuse_brdf(base_color: vec3<f32>, metallic: f32) -> vec3<f32> {
let diffuse_color = calculate_diffuse_color(base_color, metallic, 0.0, 0.0);
return diffuse_color / PI;
}

fn specular_brdf(
N: vec3<f32>,
V: vec3<f32>,
L: vec3<f32>,
base_color: vec3<f32>,
metallic: f32,
reflectance: vec3<f32>,
perceptual_roughness: f32,
roughness: f32,
) -> vec3<f32> {
let H = normalize(L + V);
let NdotL = saturate(dot(N, L));
let NdotH = saturate(dot(N, H));
let LdotH = saturate(dot(L, H));
let NdotV = max(dot(N, V), 0.0001);

let F0 = calculate_F0(base_color, metallic, reflectance);
let F_ab = F_AB(perceptual_roughness, NdotV);

let D = D_GGX(roughness, NdotH);
let Vs = V_SmithGGXCorrelated(roughness, NdotV, NdotL);
let F = fresnel(F0, LdotH);
return specular_multiscatter(D, Vs, F, F0, F_ab, 1.0);
}
1 change: 1 addition & 0 deletions crates/bevy_solari/src/scene/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub struct RaytracingScenePlugin;

impl Plugin for RaytracingScenePlugin {
fn build(&self, app: &mut App) {
load_shader_library!(app, "brdf.wgsl");
load_shader_library!(app, "raytracing_scene_bindings.wgsl");
load_shader_library!(app, "sampling.wgsl");

Expand Down
51 changes: 41 additions & 10 deletions crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
#define_import_path bevy_solari::scene_bindings

#import bevy_pbr::lighting::perceptualRoughnessToRoughness
#import bevy_pbr::pbr_functions::calculate_tbn_mikktspace

struct InstanceGeometryIds {
vertex_buffer_id: u32,
vertex_buffer_offset: u32,
index_buffer_id: u32,
index_buffer_offset: u32,
triangle_count: u32,
}

struct VertexBuffer { vertices: array<PackedVertex> }
Expand Down Expand Up @@ -34,12 +38,17 @@ fn unpack_vertex(packed: PackedVertex) -> Vertex {
}

struct Material {
base_color: vec4<f32>,
emissive: vec4<f32>,
base_color_texture_id: u32,
normal_map_texture_id: u32,
base_color_texture_id: u32,
emissive_texture_id: u32,
_padding: u32,
metallic_roughness_texture_id: u32,

base_color: vec3<f32>,
perceptual_roughness: f32,
emissive: vec3<f32>,
metallic: f32,
reflectance: vec3<f32>,
_padding: f32,
}

const TEXTURE_MAP_NONE = 0xFFFFFFFFu;
Expand Down Expand Up @@ -94,14 +103,20 @@ fn sample_texture(id: u32, uv: vec2<f32>) -> vec3<f32> {
struct ResolvedMaterial {
base_color: vec3<f32>,
emissive: vec3<f32>,
reflectance: vec3<f32>,
perceptual_roughness: f32,
roughness: f32,
metallic: f32,
}

struct ResolvedRayHitFull {
world_position: vec3<f32>,
world_normal: vec3<f32>,
geometric_world_normal: vec3<f32>,
world_tangent: vec4<f32>,
uv: vec2<f32>,
triangle_area: f32,
triangle_count: u32,
material: ResolvedMaterial,
}

Expand All @@ -118,6 +133,17 @@ fn resolve_material(material: Material, uv: vec2<f32>) -> ResolvedMaterial {
m.emissive *= sample_texture(material.emissive_texture_id, uv);
}

m.reflectance = material.reflectance;

m.perceptual_roughness = material.perceptual_roughness;
m.metallic = material.metallic;
if material.metallic_roughness_texture_id != TEXTURE_MAP_NONE {
let metallic_roughness = sample_texture(material.metallic_roughness_texture_id, uv);
m.perceptual_roughness *= metallic_roughness.g;
m.metallic *= metallic_roughness.b;
}
m.roughness = clamp(m.perceptual_roughness * m.perceptual_roughness, 0.001, 1.0);

return m;
}

Expand All @@ -144,15 +170,20 @@ fn resolve_triangle_data_full(instance_id: u32, triangle_id: u32, barycentrics:

let uv = mat3x2(vertices[0].uv, vertices[1].uv, vertices[2].uv) * barycentrics;

let local_tangent = mat3x3(vertices[0].tangent.xyz, vertices[1].tangent.xyz, vertices[2].tangent.xyz) * barycentrics;
let world_tangent = vec4(
normalize(mat3x3(transform[0].xyz, transform[1].xyz, transform[2].xyz) * local_tangent),
vertices[0].tangent.w,
);

let local_normal = mat3x3(vertices[0].normal, vertices[1].normal, vertices[2].normal) * barycentrics; // TODO: Use barycentric lerp, ray_hit.object_to_world, cross product geo normal
var world_normal = normalize(mat3x3(transform[0].xyz, transform[1].xyz, transform[2].xyz) * local_normal);
let geometric_world_normal = world_normal;
if material.normal_map_texture_id != TEXTURE_MAP_NONE {
let local_tangent = mat3x3(vertices[0].tangent.xyz, vertices[1].tangent.xyz, vertices[2].tangent.xyz) * barycentrics;
let world_tangent = normalize(mat3x3(transform[0].xyz, transform[1].xyz, transform[2].xyz) * local_tangent);
let N = world_normal;
let T = world_tangent;
let B = vertices[0].tangent.w * cross(N, T);
let TBN = calculate_tbn_mikktspace(world_normal, world_tangent);
let T = TBN[0];
let B = TBN[1];
let N = TBN[2];
let Nt = sample_texture(material.normal_map_texture_id, uv);
world_normal = normalize(Nt.x * T + Nt.y * B + Nt.z * N);
}
Expand All @@ -163,5 +194,5 @@ fn resolve_triangle_data_full(instance_id: u32, triangle_id: u32, barycentrics:

let resolved_material = resolve_material(material, uv);

return ResolvedRayHitFull(world_position, world_normal, geometric_world_normal, uv, triangle_area, resolved_material);
return ResolvedRayHitFull(world_position, world_normal, geometric_world_normal, world_tangent, uv, triangle_area, instance_geometry_ids.triangle_count, resolved_material);
}
Loading
Loading