Skip to content

Initial raytraced lighting progress (bevy_solari) #19058

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 59 commits into
base: main
Choose a base branch
from

Conversation

JMS55
Copy link
Contributor

@JMS55 JMS55 commented May 4, 2025

Bevy Solari

Preface

  • See release notes.
  • Please talk to me in #rendering-dev on discord or open a github discussion if you have questions about the long term plan, and keep discussion in this PR limited to the contents of the PR :)

Connections

This PR

After nearly two years, I've revived the raytraced lighting effort I first started in #10000.

Unlike that PR, which has realtime techniques, I've limited this PR to:

  • RaytracingScenePlugin - BLAS and TLAS building, geometry and texture binding, sampling functions.
  • PathtracingPlugin - A non-realtime path tracer intended to serve as a testbed and reference.

What's implemented?

image

  • BLAS building on mesh load
  • Emissive lights
  • Directional lights with soft shadows
  • Diffuse (lambert, not Bevy's diffuse BRDF) and emissive materials
  • A reference path tracer with:
    • Antialiasing
    • Direct light sampling (next event estimation) with 0/1 MIS weights
    • Importance-sampled BRDF bounces
    • Russian roulette

What's not implemented?

  • Anything realtime, including a real-time denoiser
  • Integration with Bevy's rasterized gbuffer
  • Specular materials
  • Non-opaque geometry
  • Any sort of CPU or GPU optimizations
    • BLAS compaction, proper bindless, and further RT APIs are things that we need wgpu to add
  • PointLights, SpotLights, or skyboxes / environment lighting
  • Support for materials other than StandardMaterial (and only a subset of properties are supported)
  • Skinned/morphed or otherwise animating/deformed meshes
  • Mipmaps
  • Adaptive self-intersection ray bias
  • A good way for developers to detect whether the user's GPU supports RT or not, and fallback to baked lighting.
  • Documentation and actual finalized APIs (literally everything is subject to change)

End-user Usage

  • Have a GPU that supports RT with inline ray queries
  • Add SolariPlugin to your app
  • Ensure any Mesh asset you want to use for raytracing has enable_raytracing: true (defaults to true), and that it uses the standard uncompressed position/normal/uv_0/tangent vertex attribute set, triangle list topology, and 32-bit indices.
    • If you don't want to build a BLAS and use the mesh for RT, set enable_raytracing to false.
  • Add the RaytracingMesh3d component to your entity (separate from Mesh3d or MeshletMesh3d).

Testing

  • Did you test these changes? If so, how?
    • Ran the solari example.
  • Are there any parts that need more testing?
    • Other test scenes probably. Normal mapping would be good to test.
  • How can other people (reviewers) test your changes? Is there anything specific they need to know?
    • See the solari.rs example for how to setup raytracing.
  • If relevant, what platforms did you test these changes on, and are there any important ones you can't test?
    • Windows 11, NVIDIA RTX 3080.

@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-Rendering Drawing game state to the screen X-Controversial There is active debate or serious implications around merging this PR M-Needs-Release-Note Work that should be called out in the blog due to impact labels May 4, 2025
@alice-i-cecile alice-i-cecile added this to the 0.17 milestone May 4, 2025
@JMS55 JMS55 marked this pull request as ready for review May 4, 2025 18:06
@alice-i-cecile alice-i-cecile added the D-Complex Quite challenging from either a design or technical perspective. Ask for help! label May 4, 2025
Comment on lines +60 to +62
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;
Copy link
Contributor

Choose a reason for hiding this comment

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

This should all just cancel out to throughput *= base_color.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes but I don't want to do that, because then it'll make things more confusing in the future.

This isn't meant to be fast, just accurate and easy to understand.

Copy link
Contributor

Choose a reason for hiding this comment

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

Probably can put the math (and cancellation) in a comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd really rather leave it explicit 😅

Comment on lines +65 to +67
let p = tonemapping_luminance(throughput);
if rand_f(&rng) > p { break; }
throughput /= p;
Copy link
Contributor

Choose a reason for hiding this comment

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

For low luminances (that don't russian roulette), this would lead to dividing by a very small number (which can lead to infs). I would probably clamp that with something like:

let p = max(0.05, 1.0 - tonemapping_luminance(throughput));
if rand_f(&rng) < p { break; }
throughput /= 1.0 - p;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll have to test this.

I had something somewhat like this before, but it lead to bias and failing the white furnace test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah this is basically what I had before, but it breaks the white furnace test. I think it's fine in practice.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's weird, it should only lead to an increase in variance, not bias.


impl SolariPlugin {
/// [`WgpuFeatures`] required for this plugin to function.
pub fn required_wgpu_features() -> WgpuFeatures {
Copy link
Contributor

Choose a reason for hiding this comment

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

(unrelated) we're really overdue for structured plugins. this is the kind of thing that should be centralized by a trait and collected at initialization to request the minimal feature set.

Copy link
Contributor

@atlv24 atlv24 left a comment

Choose a reason for hiding this comment

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

Reviewed only the rust side for now, will do wgsl later. Looks good so far!

pull_requests: [19058]
---

(TODO: Embed solari example screenshot here)
Copy link
Contributor

Choose a reason for hiding this comment

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

this TODO and the one below needs resolving

Copy link
Contributor Author

@JMS55 JMS55 May 9, 2025

Choose a reason for hiding this comment

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

Not in this PR. Instructions say not to embed images now, and that we'll do that once the release notes are transferred to the bevy website repo for the release. Otherwise we bloat this repo with multimedia.

@@ -4328,3 +4349,6 @@ name = "Extended Bindless Material"
description = "Demonstrates bindless `ExtendedMaterial`"
category = "Shaders"
wasm = false

[patch.crates-io]
naga_oil = { git = "https://github.com/JMS55/naga_oil", rev = "82476f83023da017be4963eaafc0f7b30e811bc3" }
Copy link
Contributor

Choose a reason for hiding this comment

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

we're gonna need to wait until we can use a published version here

@@ -203,6 +205,7 @@ impl Mesh {
morph_targets: None,
morph_target_names: None,
asset_usage,
enable_raytracing: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this being true mean its building a BLAS for all meshes even when bevy_solari is not in use?

Copy link
Contributor

Choose a reason for hiding this comment

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

How about instead, defaulting to false, and having a method on Mesh which checks the set of vertex attributes is {{POSITION, NORMAL, UV_0, TANGENT}} before setting the bool to true, and returns an error otherwise?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does this being true mean its building a BLAS for all meshes even when bevy_solari is not in use?

Depends by what you mean by "not in use". If the solari scene plugin is added to the app, your GPU supports RT, and the mesh is compatible and has enable_raytracing: true, then yes the mesh will get a BLAS built.

If the plugin is not loaded (it's not part of default apps) / your GPU does not support RT, or the mesh is not compatible or has RT off, then no a BLAS won't be built.

How about instead, defaulting to false, and having a method on Mesh which checks the set of vertex attributes is {{POSITION, NORMAL, UV_0, TANGENT}} before setting the bool to true, and returns an error otherwise?

The raytracing_enabled field is not meant to be "whether or not the mesh is compatible for RT". The field is meant for cases where you don't want to build a BLAS for a mesh even if it's compatible. E.g. for higher level LODs if you're using exclusively lower level LODs for RT to reduce BLAS and traversal cost. I'll update the docs to make this explicit.

min_binding_size: None,
},
count: MAX_MESH_COUNT,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought there was a shorthand api for this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not for entries with count, idt. If someone knows otherwise, let me know.

Copy link
Contributor

@atlv24 atlv24 left a comment

Choose a reason for hiding this comment

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

Very cool, good job

fn sample_cosine_hemisphere(normal: vec3<f32>, rng: ptr<function, u32>) -> vec3<f32> {
let cos_theta = 1.0 - 2.0 * rand_f(rng);
let phi = 2.0 * PI * rand_f(rng);
let sin_theta = sqrt(max(1.0 - cos_theta * cos_theta, 0.0));
Copy link
Contributor

Choose a reason for hiding this comment

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

i assume the max accounts for the precision problems the reference describes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Must be. I wrote this code a few years ago, tbh it's just copied from an older version of solari so I no longer remember.

Comment on lines +97 to +98
world_normal: vec3<f32>,
geometric_world_normal: vec3<f32>,
Copy link
Contributor

Choose a reason for hiding this comment

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

docs here one what the distinction is would be good: im guessing world_normal is the normal as dictated by the mesh, with vertex interpolation, and geometric_world_normal is the normal of the triangle.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah basically.

I'm not going to spend time documenting it because this is basically all subject to change. I haven't decided exactly what the APIs are going to look like yet because some passes probably don't even care about normals, or materials, and for shadow rays we don't have BVH intersections and have to use vertex and index buffers where for other rays we can get the positions directly from the BVH, etc.

I plan to figure things out over time and write the APIs as I need them.


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;
Copy link
Contributor

Choose a reason for hiding this comment

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

is this assuming "flat" normals? should it be taking a cross product of the edges instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure, I wrote this code a while ago. But I think I had tested on bistro at some point and it looked fine.


// Setup RNG
let pixel_index = global_id.x + global_id.y * u32(view.viewport.z);
let frame_index = u32(old_color.a) * 5782582u;
Copy link
Contributor

Choose a reason for hiding this comment

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

this doesn't look like its a frame index, plus isn't old_color.a in [0..1]? so casting it to u32 is basically always zero? so this is not really contributing anything i see old_color.a is the frame count, perhaps move the multiplication down into the rng initialization instead so that the name makes sense

@janhohenheim janhohenheim added the S-Needs-Review Needs reviewer attention (from anyone!) to move forward label May 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Rendering Drawing game state to the screen C-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! M-Needs-Release-Note Work that should be called out in the blog due to impact S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Controversial There is active debate or serious implications around merging this PR
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

5 participants