diff --git a/Cargo.toml b/Cargo.toml index a3d3a2ab63e51..8a4f79bd583cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -245,6 +245,15 @@ bevy_render = ["bevy_internal/bevy_render", "bevy_color"] # Provides scene functionality bevy_scene = ["bevy_internal/bevy_scene", "bevy_asset"] +# Provides raytraced lighting (do not use, not yet ready for users) +bevy_solari = [ + "bevy_internal/bevy_solari", + "bevy_asset", + "bevy_core_pipeline", + "bevy_pbr", + "bevy_render", +] + # Provides sprite functionality bevy_sprite = [ "bevy_internal/bevy_sprite", @@ -1245,6 +1254,18 @@ description = "Load a cubemap texture onto a cube like a skybox and cycle throug category = "3D Rendering" wasm = false +[[example]] +name = "solari" +path = "examples/3d/solari.rs" +doc-scrape-examples = true +required-features = ["bevy_solari"] + +[package.metadata.example.solari] +name = "Solari" +description = "Demonstrates realtime dynamic global illumination rendering using Bevy Solari." +category = "3D Rendering" +wasm = false + [[example]] name = "spherical_area_lights" path = "examples/3d/spherical_area_lights.rs" diff --git a/assets/branding/bevy_solari.svg b/assets/branding/bevy_solari.svg new file mode 100644 index 0000000000000..c4fd352f55d44 --- /dev/null +++ b/assets/branding/bevy_solari.svg @@ -0,0 +1,303 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/models/CornellBox/box_modified.glb b/assets/models/CornellBox/box_modified.glb new file mode 100644 index 0000000000000..0e697de45c6af Binary files /dev/null and b/assets/models/CornellBox/box_modified.glb differ diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 28d234f2b4e3e..62a41b3ae5f72 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -414,6 +414,7 @@ bevy_picking = { path = "../bevy_picking", optional = true, version = "0.16.0-de bevy_remote = { path = "../bevy_remote", optional = true, version = "0.16.0-dev" } bevy_render = { path = "../bevy_render", optional = true, version = "0.16.0-dev" } bevy_scene = { path = "../bevy_scene", optional = true, version = "0.16.0-dev" } +bevy_solari = { path = "../bevy_solari", optional = true, version = "0.16.0-dev" } bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.16.0-dev" } bevy_state = { path = "../bevy_state", optional = true, version = "0.16.0-dev", default-features = false, features = [ "bevy_app", diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 07dd936ab1dcc..9c21ab14a3927 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -60,6 +60,8 @@ pub use bevy_remote as remote; pub use bevy_render as render; #[cfg(feature = "bevy_scene")] pub use bevy_scene as scene; +#[cfg(feature = "bevy_solari")] +pub use bevy_solari as solari; #[cfg(feature = "bevy_sprite")] pub use bevy_sprite as sprite; #[cfg(feature = "bevy_state")] diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index e4868dbf6997d..707f6ff9e3328 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -119,6 +119,18 @@ pub struct Mesh { morph_targets: Option>, morph_target_names: Option>, pub asset_usage: RenderAssetUsages, + /// Whether or not to build a BLAS for use with `bevy_solari` raytracing. + /// + /// Note that this is _not_ whether the mesh is _compatible_ with `bevy_solari` raytracing. + /// This field just controls whether or not a BLAS gets built for this mesh, assuming that + /// the mesh is compatible. + /// + /// The use case for this field is setting it to true for low resolution proxy meshes you want to use for raytracing, + /// and false for higher resolution versions of the mesh that you want to use for raster. + /// + /// Does nothing if not used with `bevy_solari`, or if the mesh is not compatible + /// with `bevy_solari` (see `bevy_solari`'s docs). + pub enable_raytracing: bool, } impl Mesh { @@ -203,6 +215,7 @@ impl Mesh { morph_targets: None, morph_target_names: None, asset_usage, + enable_raytracing: true, } } diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index aa6b6e239cbfd..f42999bcd2492 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -117,7 +117,7 @@ wesl = { version = "0.1.2", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Omit the `glsl` feature in non-WebAssembly by default. -naga_oil = { version = "0.17", default-features = false, features = [ +naga_oil = { version = "0.17.1", default-features = false, features = [ "test_shader", ] } @@ -125,7 +125,7 @@ naga_oil = { version = "0.17", default-features = false, features = [ proptest = "1" [target.'cfg(target_arch = "wasm32")'.dependencies] -naga_oil = "0.17" +naga_oil = "0.17.1" js-sys = "0.3" web-sys = { version = "0.3.67", features = [ 'Blob', diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 95218b7a593cd..b7250988e7f57 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -1064,6 +1064,7 @@ pub fn camera_system( #[reflect(opaque)] #[reflect(Component, Default, Clone)] pub struct CameraMainTextureUsages(pub TextureUsages); + impl Default for CameraMainTextureUsages { fn default() -> Self { Self( @@ -1074,6 +1075,13 @@ impl Default for CameraMainTextureUsages { } } +impl CameraMainTextureUsages { + pub fn with(mut self, usages: TextureUsages) -> Self { + self.0 |= usages; + self + } +} + #[derive(Component, Debug)] pub struct ExtractedCamera { pub target: Option, diff --git a/crates/bevy_render/src/mesh/allocator.rs b/crates/bevy_render/src/mesh/allocator.rs index eb2d4de626e1a..c171cf3957d96 100644 --- a/crates/bevy_render/src/mesh/allocator.rs +++ b/crates/bevy_render/src/mesh/allocator.rs @@ -78,6 +78,9 @@ pub struct MeshAllocator { /// WebGL 2. On this platform, we must give each vertex array its own /// buffer, because we can't adjust the first vertex when we perform a draw. general_vertex_slabs_supported: bool, + + /// Additional buffer usages to add to any vertex or index buffers created. + pub extra_buffer_usages: BufferUsages, } /// Tunable parameters that customize the behavior of the allocator. @@ -348,6 +351,7 @@ impl FromWorld for MeshAllocator { mesh_id_to_index_slab: HashMap::default(), next_slab_id: default(), general_vertex_slabs_supported, + extra_buffer_usages: BufferUsages::empty(), } } } @@ -598,7 +602,7 @@ impl MeshAllocator { buffer_usages_to_str(buffer_usages) )), size: len as u64, - usage: buffer_usages | BufferUsages::COPY_DST, + usage: buffer_usages | BufferUsages::COPY_DST | self.extra_buffer_usages, mapped_at_creation: true, }); { @@ -835,7 +839,7 @@ impl MeshAllocator { buffer_usages_to_str(buffer_usages) )), size: slab.current_slot_capacity as u64 * slab.element_layout.slot_size(), - usage: buffer_usages, + usage: buffer_usages | self.extra_buffer_usages, mapped_at_creation: false, }); diff --git a/crates/bevy_render/src/render_resource/bind_group_entries.rs b/crates/bevy_render/src/render_resource/bind_group_entries.rs index 3aaf46183ffb1..cc8eb188de461 100644 --- a/crates/bevy_render/src/render_resource/bind_group_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_entries.rs @@ -147,6 +147,13 @@ impl<'a> IntoBinding<'a> for &'a TextureView { } } +impl<'a> IntoBinding<'a> for &'a wgpu::TextureView { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::TextureView(self) + } +} + impl<'a> IntoBinding<'a> for &'a [&'a wgpu::TextureView] { #[inline] fn into_binding(self) -> BindingResource<'a> { @@ -161,6 +168,13 @@ impl<'a> IntoBinding<'a> for &'a Sampler { } } +impl<'a> IntoBinding<'a> for &'a [&'a wgpu::Sampler] { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::SamplerArray(self) + } +} + impl<'a> IntoBinding<'a> for BindingResource<'a> { #[inline] fn into_binding(self) -> BindingResource<'a> { @@ -175,6 +189,13 @@ impl<'a> IntoBinding<'a> for wgpu::BufferBinding<'a> { } } +impl<'a> IntoBinding<'a> for &'a [wgpu::BufferBinding<'a>] { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::BufferArray(self) + } +} + pub trait IntoBindingArray<'b, const N: usize> { fn into_array(self) -> [BindingResource<'b>; N]; } diff --git a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs index bc4a7d306da4b..41affa434959a 100644 --- a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs @@ -568,4 +568,8 @@ pub mod binding_types { } .into_bind_group_layout_entry_builder() } + + pub fn acceleration_structure() -> BindGroupLayoutEntryBuilder { + BindingType::AccelerationStructure.into_bind_group_layout_entry_builder() + } } diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index b777d96290ccd..aecf27173d9bb 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -38,18 +38,21 @@ pub use wgpu::{ BufferInitDescriptor, DispatchIndirectArgs, DrawIndexedIndirectArgs, DrawIndirectArgs, TextureDataOrder, }, - AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, AstcChannel, BindGroupDescriptor, - BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, + AccelerationStructureFlags, AccelerationStructureGeometryFlags, + AccelerationStructureUpdateMode, AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, + AstcChannel, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, + BindGroupLayoutEntry, BindingResource, BindingType, Blas, BlasBuildEntry, BlasGeometries, + BlasGeometrySizeDescriptors, BlasTriangleGeometry, BlasTriangleGeometrySizeDescriptor, BlendComponent, BlendFactor, BlendOperation, BlendState, BufferAddress, BufferAsyncError, BufferBinding, BufferBindingType, BufferDescriptor, BufferSize, BufferUsages, ColorTargetState, ColorWrites, CommandEncoder, CommandEncoderDescriptor, CompareFunction, ComputePass, ComputePassDescriptor, ComputePipelineDescriptor as RawComputePipelineDescriptor, - DepthBiasState, DepthStencilState, DownlevelFlags, Extent3d, Face, Features as WgpuFeatures, - FilterMode, FragmentState as RawFragmentState, FrontFace, ImageSubresourceRange, IndexFormat, - Limits as WgpuLimits, LoadOp, Maintain, MapMode, MultisampleState, Operations, Origin3d, - PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor, PolygonMode, - PrimitiveState, PrimitiveTopology, PushConstantRange, RenderPassColorAttachment, - RenderPassDepthStencilAttachment, RenderPassDescriptor, + CreateBlasDescriptor, CreateTlasDescriptor, DepthBiasState, DepthStencilState, DownlevelFlags, + Extent3d, Face, Features as WgpuFeatures, FilterMode, FragmentState as RawFragmentState, + FrontFace, ImageSubresourceRange, IndexFormat, Limits as WgpuLimits, LoadOp, Maintain, MapMode, + MultisampleState, Operations, Origin3d, PipelineCompilationOptions, PipelineLayout, + PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, PushConstantRange, + RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipelineDescriptor as RawRenderPipelineDescriptor, Sampler as WgpuSampler, SamplerBindingType, SamplerBindingType as WgpuSamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, StencilFaceState, @@ -57,8 +60,9 @@ pub use wgpu::{ TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureFormatFeatureFlags, TextureFormatFeatures, TextureSampleType, TextureUsages, TextureView as WgpuTextureView, TextureViewDescriptor, - TextureViewDimension, VertexAttribute, VertexBufferLayout as RawVertexBufferLayout, - VertexFormat, VertexState as RawVertexState, VertexStepMode, COPY_BUFFER_ALIGNMENT, + TextureViewDimension, Tlas, TlasInstance, TlasPackage, VertexAttribute, + VertexBufferLayout as RawVertexBufferLayout, VertexFormat, VertexState as RawVertexState, + VertexStepMode, COPY_BUFFER_ALIGNMENT, }; pub use crate::mesh::VertexBufferLayout; diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 7c54b0f4069d2..20939cd379f74 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -1197,6 +1197,10 @@ fn get_capabilities(features: Features, downlevel: DownlevelFlags) -> Capabiliti Capabilities::MULTISAMPLED_SHADING, downlevel.contains(DownlevelFlags::MULTISAMPLED_SHADING), ); + capabilities.set( + Capabilities::RAY_QUERY, + features.contains(Features::EXPERIMENTAL_RAY_QUERY), + ); capabilities.set( Capabilities::DUAL_SOURCE_BLENDING, features.contains(Features::DUAL_SOURCE_BLENDING), diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index 1691911c2cbbe..12319114e74b9 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -219,12 +219,6 @@ pub async fn initialize_renderer( features -= wgpu::Features::MAPPABLE_PRIMARY_BUFFERS; } - // RAY_QUERY and RAY_TRACING_ACCELERATION STRUCTURE will sometimes cause DeviceLost failures on platforms - // that report them as supported: - // - features -= wgpu::Features::EXPERIMENTAL_RAY_QUERY; - features -= wgpu::Features::EXPERIMENTAL_RAY_TRACING_ACCELERATION_STRUCTURE; - limits = adapter.limits(); } diff --git a/crates/bevy_solari/Cargo.toml b/crates/bevy_solari/Cargo.toml new file mode 100644 index 0000000000000..0e5f682986476 --- /dev/null +++ b/crates/bevy_solari/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "bevy_solari" +version = "0.16.0-dev" +edition = "2024" +description = "Provides raytraced lighting for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } +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_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" } +bevy_pbr = { path = "../bevy_pbr", version = "0.16.0-dev" } # TODO: Ideally remove this dependency +bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ + "std", +] } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } +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"] } +derive_more = { version = "1", default-features = false, features = ["from"] } + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_solari/LICENSE-APACHE b/crates/bevy_solari/LICENSE-APACHE new file mode 100644 index 0000000000000..d9a10c0d8e868 --- /dev/null +++ b/crates/bevy_solari/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/bevy_solari/LICENSE-MIT b/crates/bevy_solari/LICENSE-MIT new file mode 100644 index 0000000000000..9cf106272ac3b --- /dev/null +++ b/crates/bevy_solari/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/bevy_solari/README.md b/crates/bevy_solari/README.md new file mode 100644 index 0000000000000..089418e60d197 --- /dev/null +++ b/crates/bevy_solari/README.md @@ -0,0 +1,9 @@ +# Bevy Solari + +[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) +[![Crates.io](https://img.shields.io/crates/v/bevy_solari.svg)](https://crates.io/crates/bevy_solari) +[![Downloads](https://img.shields.io/crates/d/bevy_solari.svg)](https://crates.io/crates/bevy_solari) +[![Docs](https://docs.rs/bevy_solari/badge.svg)](https://docs.rs/bevy_solari/latest/bevy_solari/) +[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) + +![Logo](../../assets/branding/bevy_solari.svg) diff --git a/crates/bevy_solari/src/lib.rs b/crates/bevy_solari/src/lib.rs new file mode 100644 index 0000000000000..fea63f34009d5 --- /dev/null +++ b/crates/bevy_solari/src/lib.rs @@ -0,0 +1,42 @@ +#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] + +//! Provides raytraced lighting. +//! +//! ![`bevy_solari` logo](https://raw.githubusercontent.com/bevyengine/bevy/assets/branding/bevy_solari.svg) +pub mod pathtracer; +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::scene::RaytracingMesh3d; +} + +use bevy_app::{App, Plugin}; +use bevy_render::settings::WgpuFeatures; +use pathtracer::PathtracingPlugin; +use scene::RaytracingScenePlugin; + +pub struct SolariPlugin; + +impl Plugin for SolariPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((RaytracingScenePlugin, PathtracingPlugin)); + } +} + +impl SolariPlugin { + /// [`WgpuFeatures`] required for this plugin to function. + pub fn required_wgpu_features() -> WgpuFeatures { + WgpuFeatures::EXPERIMENTAL_RAY_TRACING_ACCELERATION_STRUCTURE + | WgpuFeatures::EXPERIMENTAL_RAY_QUERY + | WgpuFeatures::BUFFER_BINDING_ARRAY + | WgpuFeatures::TEXTURE_BINDING_ARRAY + | WgpuFeatures::UNIFORM_BUFFER_AND_STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING + | WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING + | WgpuFeatures::PARTIALLY_BOUND_BINDING_ARRAY + } +} diff --git a/crates/bevy_solari/src/pathtracer/extract.rs b/crates/bevy_solari/src/pathtracer/extract.rs new file mode 100644 index 0000000000000..8404cb6cf2152 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/extract.rs @@ -0,0 +1,33 @@ +use super::{prepare::PathtracerAccumulationTexture, Pathtracer}; +use bevy_ecs::{ + change_detection::DetectChanges, + system::{Commands, Query}, + world::Ref, +}; +use bevy_render::{camera::Camera, sync_world::RenderEntity, Extract}; +use bevy_transform::components::GlobalTransform; + +pub fn extract_pathtracer( + cameras_3d: Extract< + Query<( + RenderEntity, + &Camera, + Ref, + Option<&Pathtracer>, + )>, + >, + mut commands: Commands, +) { + for (entity, camera, global_transform, pathtracer) in &cameras_3d { + let mut entity_commands = commands + .get_entity(entity) + .expect("Camera entity wasn't synced."); + if pathtracer.is_some() && camera.is_active && camera.hdr { + let mut pathtracer: Pathtracer = pathtracer.unwrap().clone(); + pathtracer.reset |= global_transform.is_changed(); + entity_commands.insert(pathtracer); + } else { + entity_commands.remove::<(Pathtracer, PathtracerAccumulationTexture)>(); + } + } +} diff --git a/crates/bevy_solari/src/pathtracer/mod.rs b/crates/bevy_solari/src/pathtracer/mod.rs new file mode 100644 index 0000000000000..5c047c51f3133 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/mod.rs @@ -0,0 +1,70 @@ +mod extract; +mod node; +mod prepare; + +use crate::SolariPlugin; +use bevy_app::{App, Plugin}; +use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; +use bevy_ecs::{component::Component, reflect::ReflectComponent, schedule::IntoScheduleConfigs}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + render_graph::{RenderGraphApp, ViewNodeRunner}, + render_resource::Shader, + renderer::RenderDevice, + ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use extract::extract_pathtracer; +use node::PathtracerNode; +use prepare::prepare_pathtracer_accumulation_texture; +use tracing::warn; + +const PATHTRACER_SHADER_HANDLE: Handle = + weak_handle!("87a5940d-b1ba-4cae-b8ce-be20c931e0c7"); + +pub struct PathtracingPlugin; + +impl Plugin for PathtracingPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + PATHTRACER_SHADER_HANDLE, + "pathtracer.wgsl", + Shader::from_wgsl + ); + + app.register_type::(); + } + + fn finish(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + + let render_device = render_app.world().resource::(); + let features = render_device.features(); + if !features.contains(SolariPlugin::required_wgpu_features()) { + warn!( + "PathtracingPlugin not loaded. GPU lacks support for required features: {:?}.", + SolariPlugin::required_wgpu_features().difference(features) + ); + return; + } + + render_app + .add_systems(ExtractSchedule, extract_pathtracer) + .add_systems( + Render, + prepare_pathtracer_accumulation_texture.in_set(RenderSystems::PrepareResources), + ) + .add_render_graph_node::>( + Core3d, + node::graph::PathtracerNode, + ) + .add_render_graph_edges(Core3d, (Node3d::EndMainPass, node::graph::PathtracerNode)); + } +} + +#[derive(Component, Reflect, Default, Clone)] +#[reflect(Component, Default, Clone)] +pub struct Pathtracer { + pub reset: bool, +} diff --git a/crates/bevy_solari/src/pathtracer/node.rs b/crates/bevy_solari/src/pathtracer/node.rs new file mode 100644 index 0000000000000..d9e60e4b44e4f --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/node.rs @@ -0,0 +1,133 @@ +use super::{prepare::PathtracerAccumulationTexture, Pathtracer, PATHTRACER_SHADER_HANDLE}; +use crate::scene::RaytracingSceneBindings; +use bevy_ecs::{ + query::QueryItem, + world::{FromWorld, World}, +}; +use bevy_render::{ + camera::ExtractedCamera, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{ + binding_types::{texture_storage_2d, uniform_buffer}, + BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedComputePipelineId, + ComputePassDescriptor, ComputePipelineDescriptor, ImageSubresourceRange, PipelineCache, + ShaderStages, StorageTextureAccess, TextureFormat, + }, + renderer::{RenderContext, RenderDevice}, + view::{ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, +}; + +pub mod graph { + use bevy_render::render_graph::RenderLabel; + + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] + pub struct PathtracerNode; +} + +pub struct PathtracerNode { + bind_group_layout: BindGroupLayout, + pipeline: CachedComputePipelineId, +} + +impl ViewNode for PathtracerNode { + type ViewQuery = ( + &'static Pathtracer, + &'static PathtracerAccumulationTexture, + &'static ExtractedCamera, + &'static ViewTarget, + &'static ViewUniformOffset, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (pathtracer, accumulation_texture, camera, view_target, view_uniform_offset): QueryItem< + Self::ViewQuery, + >, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let scene_bindings = world.resource::(); + let view_uniforms = world.resource::(); + let (Some(pipeline), Some(scene_bindings), Some(viewport), Some(view_uniforms)) = ( + pipeline_cache.get_compute_pipeline(self.pipeline), + &scene_bindings.bind_group, + camera.physical_viewport_size, + view_uniforms.uniforms.binding(), + ) else { + return Ok(()); + }; + + let bind_group = render_context.render_device().create_bind_group( + "pathtracer_bind_group", + &self.bind_group_layout, + &BindGroupEntries::sequential(( + &accumulation_texture.0.default_view, + view_target.get_unsampled_color_attachment().view, + view_uniforms, + )), + ); + + let command_encoder = render_context.command_encoder(); + + if pathtracer.reset { + command_encoder.clear_texture( + &accumulation_texture.0.texture, + &ImageSubresourceRange::default(), + ); + } + + let mut pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("pathtracer"), + timestamp_writes: None, + }); + pass.set_pipeline(pipeline); + pass.set_bind_group(0, scene_bindings, &[]); + pass.set_bind_group(1, &bind_group, &[view_uniform_offset.offset]); + pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1); + + Ok(()) + } +} + +impl FromWorld for PathtracerNode { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let pipeline_cache = world.resource::(); + let scene_bindings = world.resource::(); + + let bind_group_layout = render_device.create_bind_group_layout( + "pathtracer_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_storage_2d(TextureFormat::Rgba32Float, StorageTextureAccess::ReadWrite), + texture_storage_2d( + ViewTarget::TEXTURE_FORMAT_HDR, + StorageTextureAccess::WriteOnly, + ), + uniform_buffer::(true), + ), + ), + ); + + let pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("pathtracer_pipeline".into()), + layout: vec![ + scene_bindings.bind_group_layout.clone(), + bind_group_layout.clone(), + ], + push_constant_ranges: vec![], + shader: PATHTRACER_SHADER_HANDLE, + shader_defs: vec![], + entry_point: "pathtrace".into(), + zero_initialize_workgroup_memory: false, + }); + + Self { + bind_group_layout, + pipeline, + } + } +} diff --git a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl new file mode 100644 index 0000000000000..d97dc3e242db0 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl @@ -0,0 +1,78 @@ +#import bevy_core_pipeline::tonemapping::tonemapping_luminance +#import bevy_pbr::utils::{rand_f, rand_vec2f} +#import bevy_render::maths::PI +#import bevy_render::view::View +#import bevy_solari::sampling::{sample_random_light, sample_cosine_hemisphere} +#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN, RAY_T_MAX} + +@group(1) @binding(0) var accumulation_texture: texture_storage_2d; +@group(1) @binding(1) var view_output: texture_storage_2d; +@group(1) @binding(2) var view: View; + +@compute @workgroup_size(8, 8, 1) +fn pathtrace(@builtin(global_invocation_id) global_id: vec3) { + if any(global_id.xy >= vec2u(view.viewport.zw)) { + return; + } + + let old_color = textureLoad(accumulation_texture, global_id.xy); + + // Setup RNG + let pixel_index = global_id.x + global_id.y * u32(view.viewport.z); + let frame_index = u32(old_color.a) * 5782582u; + var rng = pixel_index + frame_index; + + // Shoot the first ray from the camera + let pixel_center = vec2(global_id.xy) + 0.5; + let jitter = rand_vec2f(&rng) - 0.5; + let pixel_uv = (pixel_center + jitter) / view.viewport.zw; + let pixel_ndc = (pixel_uv * 2.0) - 1.0; + let primary_ray_target = view.world_from_clip * vec4(pixel_ndc.x, -pixel_ndc.y, 1.0, 1.0); + var ray_origin = view.world_position; + var ray_direction = normalize((primary_ray_target.xyz / primary_ray_target.w) - ray_origin); + var ray_t_min = 0.0; + + // Path trace + var radiance = vec3(0.0); + var throughput = vec3(1.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); + + // Evaluate material BRDF + let diffuse_brdf = ray_hit.material.base_color / PI; + + // 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 + radiance += throughput * diffuse_brdf * sample_random_light(ray_hit.world_position, ray_hit.world_normal, &rng); + + // 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 + ray_origin = ray_hit.world_position; + ray_t_min = RAY_T_MIN; + + // 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; + + // Russian roulette for early termination + let p = tonemapping_luminance(throughput); + if rand_f(&rng) > p { break; } + throughput /= p; + } else { break; } + } + + // Camera exposure + radiance *= view.exposure; + + // Accumulation over time via running average + let new_color = mix(old_color.rgb, radiance, 1.0 / (old_color.a + 1.0)); + 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)); +} diff --git a/crates/bevy_solari/src/pathtracer/prepare.rs b/crates/bevy_solari/src/pathtracer/prepare.rs new file mode 100644 index 0000000000000..7ef4733124bdb --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/prepare.rs @@ -0,0 +1,52 @@ +use super::Pathtracer; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::With, + system::{Commands, Query, Res, ResMut}, +}; +use bevy_render::{ + camera::ExtractedCamera, + render_resource::{ + Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, + }, + renderer::RenderDevice, + texture::{CachedTexture, TextureCache}, +}; + +#[derive(Component)] +pub struct PathtracerAccumulationTexture(pub CachedTexture); + +pub fn prepare_pathtracer_accumulation_texture( + query: Query<(Entity, &ExtractedCamera), With>, + mut texture_cache: ResMut, + render_device: Res, + mut commands: Commands, +) { + for (entity, camera) in &query { + let Some(viewport) = camera.physical_viewport_size else { + continue; + }; + + let descriptor = TextureDescriptor { + label: Some("pathtracer_accumulation_texture"), + size: Extent3d { + width: viewport.x, + height: viewport.y, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba32Float, + usage: TextureUsages::STORAGE_BINDING, + view_formats: &[], + }; + + commands + .entity(entity) + .insert(PathtracerAccumulationTexture( + texture_cache.get(&render_device, descriptor), + )); + } +} diff --git a/crates/bevy_solari/src/scene/binder.rs b/crates/bevy_solari/src/scene/binder.rs new file mode 100644 index 0000000000000..8e548b486dd9a --- /dev/null +++ b/crates/bevy_solari/src/scene/binder.rs @@ -0,0 +1,353 @@ +use super::{blas::BlasManager, extract::StandardMaterialAssets, RaytracingMesh3d}; +use bevy_asset::{AssetId, Handle}; +use bevy_color::LinearRgba; +use bevy_ecs::{ + resource::Resource, + system::{Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_math::{ops::cos, Mat4, Vec3}; +use bevy_pbr::{ExtractedDirectionalLight, MeshMaterial3d, StandardMaterial}; +use bevy_platform::{collections::HashMap, hash::FixedHasher}; +use bevy_render::{ + mesh::allocator::MeshAllocator, + render_asset::RenderAssets, + render_resource::{binding_types::*, *}, + renderer::{RenderDevice, RenderQueue}, + texture::{FallbackImage, GpuImage}, +}; +use bevy_transform::components::GlobalTransform; +use core::{hash::Hash, num::NonZeroU32, ops::Deref}; + +const MAX_MESH_SLAB_COUNT: NonZeroU32 = NonZeroU32::new(500).unwrap(); +const MAX_TEXTURE_COUNT: NonZeroU32 = NonZeroU32::new(5_000).unwrap(); + +/// Average angular diameter of the sun as seen from earth. +/// +const SUN_ANGULAR_DIAMETER_RADIANS: f32 = 0.00930842; + +#[derive(Resource)] +pub struct RaytracingSceneBindings { + pub bind_group: Option, + pub bind_group_layout: BindGroupLayout, +} + +pub fn prepare_raytracing_scene_bindings( + instances_query: Query<( + &RaytracingMesh3d, + &MeshMaterial3d, + &GlobalTransform, + )>, + directional_lights_query: Query<&ExtractedDirectionalLight>, + mesh_allocator: Res, + blas_manager: Res, + material_assets: Res, + texture_assets: Res>, + fallback_texture: Res, + render_device: Res, + render_queue: Res, + mut raytracing_scene_bindings: ResMut, +) { + raytracing_scene_bindings.bind_group = None; + + if instances_query.iter().len() == 0 { + return; + } + + let mut vertex_buffers = CachedBindingArray::new(); + let mut index_buffers = CachedBindingArray::new(); + let mut textures = CachedBindingArray::new(); + let mut samplers = Vec::new(); + let mut materials = StorageBufferList::::default(); + let mut tlas = TlasPackage::new(render_device.wgpu_device().create_tlas( + &CreateTlasDescriptor { + label: Some("tlas"), + flags: AccelerationStructureFlags::PREFER_FAST_TRACE, + update_mode: AccelerationStructureUpdateMode::Build, + max_instances: instances_query.iter().len() as u32, + }, + )); + let mut transforms = StorageBufferList::::default(); + let mut geometry_ids = StorageBufferList::::default(); + let mut material_ids = StorageBufferList::::default(); + let mut light_sources = StorageBufferList::::default(); + let mut directional_lights = StorageBufferList::::default(); + + let mut material_id_map: HashMap, u32, FixedHasher> = + HashMap::default(); + let mut material_id = 0; + let mut process_texture = |texture_handle: &Option>| -> Option { + match texture_handle { + Some(texture_handle) => match texture_assets.get(texture_handle.id()) { + Some(texture) => { + let (texture_id, is_new) = + textures.push_if_absent(texture.texture_view.deref(), texture_handle.id()); + if is_new { + samplers.push(texture.sampler.deref()); + } + Some(texture_id) + } + None => None, + }, + None => Some(u32::MAX), + } + }; + for (asset_id, material) in material_assets.iter() { + let Some(base_color_texture_id) = process_texture(&material.base_color_texture) else { + continue; + }; + let Some(normal_map_texture_id) = process_texture(&material.normal_map_texture) else { + continue; + }; + let Some(emissive_texture_id) = process_texture(&material.emissive_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, + emissive_texture_id, + _padding: Default::default(), + }); + + material_id_map.insert(*asset_id, material_id); + material_id += 1; + } + + if material_id == 0 { + return; + } + + if textures.is_empty() { + textures.vec.push(fallback_texture.d2.texture_view.deref()); + samplers.push(fallback_texture.d2.sampler.deref()); + } + + let mut instance_id = 0; + for (mesh, material, transform) in &instances_query { + let Some(blas) = blas_manager.get(&mesh.id()) else { + continue; + }; + let Some(vertex_slice) = mesh_allocator.mesh_vertex_slice(&mesh.id()) else { + continue; + }; + let Some(index_slice) = mesh_allocator.mesh_index_slice(&mesh.id()) else { + continue; + }; + let Some(material_id) = material_id_map.get(&material.id()).copied() else { + continue; + }; + let Some(material) = materials.get().get(material_id as usize) else { + continue; + }; + + let transform = transform.compute_matrix(); + *tlas.get_mut_single(instance_id).unwrap() = Some(TlasInstance::new( + blas, + tlas_transform(&transform), + Default::default(), + 0xFF, + )); + + transforms.get_mut().push(transform); + + let (vertex_buffer_id, _) = vertex_buffers.push_if_absent( + vertex_slice.buffer.as_entire_buffer_binding(), + vertex_slice.buffer.id(), + ); + let (index_buffer_id, _) = index_buffers.push_if_absent( + index_slice.buffer.as_entire_buffer_binding(), + index_slice.buffer.id(), + ); + + geometry_ids.get_mut().push(GpuInstanceGeometryIds { + vertex_buffer_id, + vertex_buffer_offset: vertex_slice.range.start, + index_buffer_id, + index_buffer_offset: index_slice.range.start, + }); + + material_ids.get_mut().push(material_id); + + if material.emissive != LinearRgba::BLACK { + light_sources + .get_mut() + .push(GpuLightSource::new_emissive_mesh_light( + instance_id as u32, + (index_slice.range.len() / 3) as u32, + )); + } + + instance_id += 1; + } + + if instance_id == 0 { + return; + } + + for directional_light in &directional_lights_query { + let directional_lights = directional_lights.get_mut(); + let directional_light_id = directional_lights.len() as u32; + + directional_lights.push(GpuDirectionalLight { + direction_to_light: directional_light.transform.back().into(), + cos_theta_max: cos(SUN_ANGULAR_DIAMETER_RADIANS / 2.0), + illuminance: directional_light.color * directional_light.illuminance, + }); + + light_sources + .get_mut() + .push(GpuLightSource::new_directional_light(directional_light_id)); + } + + materials.write_buffer(&render_device, &render_queue); + transforms.write_buffer(&render_device, &render_queue); + geometry_ids.write_buffer(&render_device, &render_queue); + material_ids.write_buffer(&render_device, &render_queue); + light_sources.write_buffer(&render_device, &render_queue); + directional_lights.write_buffer(&render_device, &render_queue); + + let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("build_tlas_command_encoder"), + }); + command_encoder.build_acceleration_structures(&[], [&tlas]); + render_queue.submit([command_encoder.finish()]); + + raytracing_scene_bindings.bind_group = Some(render_device.create_bind_group( + "raytracing_scene_bind_group", + &raytracing_scene_bindings.bind_group_layout, + &BindGroupEntries::sequential(( + vertex_buffers.as_slice(), + index_buffers.as_slice(), + textures.as_slice(), + samplers.as_slice(), + materials.binding().unwrap(), + tlas.as_binding(), + transforms.binding().unwrap(), + geometry_ids.binding().unwrap(), + material_ids.binding().unwrap(), + light_sources.binding().unwrap(), + directional_lights.binding().unwrap(), + )), + )); +} + +impl FromWorld for RaytracingSceneBindings { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + Self { + bind_group: None, + bind_group_layout: render_device.create_bind_group_layout( + "raytracing_scene_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_read_only_sized(false, None).count(MAX_MESH_SLAB_COUNT), + storage_buffer_read_only_sized(false, None).count(MAX_MESH_SLAB_COUNT), + texture_2d(TextureSampleType::Float { filterable: true }) + .count(MAX_TEXTURE_COUNT), + sampler(SamplerBindingType::Filtering).count(MAX_TEXTURE_COUNT), + storage_buffer_read_only_sized(false, None), + acceleration_structure(), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + } + } +} + +struct CachedBindingArray { + map: HashMap, + vec: Vec, +} + +impl CachedBindingArray { + fn new() -> Self { + Self { + map: HashMap::default(), + vec: Vec::default(), + } + } + + fn push_if_absent(&mut self, item: T, item_id: I) -> (u32, bool) { + let mut is_new = false; + let i = *self.map.entry(item_id).or_insert_with(|| { + is_new = true; + let i = self.vec.len() as u32; + self.vec.push(item); + i + }); + (i, is_new) + } + + fn is_empty(&self) -> bool { + self.vec.is_empty() + } + + fn as_slice(&self) -> &[T] { + self.vec.as_slice() + } +} + +type StorageBufferList = StorageBuffer>; + +#[derive(ShaderType)] +struct GpuInstanceGeometryIds { + vertex_buffer_id: u32, + vertex_buffer_offset: u32, + index_buffer_id: u32, + index_buffer_offset: u32, +} + +#[derive(ShaderType)] +struct GpuMaterial { + base_color: LinearRgba, + emissive: LinearRgba, + base_color_texture_id: u32, + normal_map_texture_id: u32, + emissive_texture_id: u32, + _padding: u32, +} + +#[derive(ShaderType)] +struct GpuLightSource { + kind: u32, + id: u32, +} + +impl GpuLightSource { + fn new_emissive_mesh_light(instance_id: u32, triangle_count: u32) -> GpuLightSource { + Self { + kind: triangle_count << 1, + id: instance_id, + } + } + + fn new_directional_light(directional_light_id: u32) -> GpuLightSource { + Self { + kind: 1, + id: directional_light_id, + } + } +} + +#[derive(ShaderType, Default)] +struct GpuDirectionalLight { + direction_to_light: Vec3, + cos_theta_max: f32, + illuminance: LinearRgba, +} + +fn tlas_transform(transform: &Mat4) -> [f32; 12] { + transform.transpose().to_cols_array()[..12] + .try_into() + .unwrap() +} diff --git a/crates/bevy_solari/src/scene/blas.rs b/crates/bevy_solari/src/scene/blas.rs new file mode 100644 index 0000000000000..5beaa3b57c4a4 --- /dev/null +++ b/crates/bevy_solari/src/scene/blas.rs @@ -0,0 +1,133 @@ +use bevy_asset::AssetId; +use bevy_ecs::{ + resource::Resource, + system::{Res, ResMut}, +}; +use bevy_mesh::{Indices, Mesh}; +use bevy_platform::collections::HashMap; +use bevy_render::{ + mesh::{ + allocator::{MeshAllocator, MeshBufferSlice}, + RenderMesh, + }, + render_asset::ExtractedAssets, + render_resource::*, + renderer::{RenderDevice, RenderQueue}, +}; + +#[derive(Resource, Default)] +pub struct BlasManager(HashMap, Blas>); + +impl BlasManager { + pub fn get(&self, mesh: &AssetId) -> Option<&Blas> { + self.0.get(mesh) + } +} + +pub fn prepare_raytracing_blas( + mut blas_manager: ResMut, + extracted_meshes: Res>, + mesh_allocator: Res, + render_device: Res, + render_queue: Res, +) { + let blas_manager = &mut blas_manager.0; + + // Delete BLAS for deleted or modified meshes + for asset_id in extracted_meshes + .removed + .iter() + .chain(extracted_meshes.modified.iter()) + { + blas_manager.remove(asset_id); + } + + if extracted_meshes.extracted.is_empty() { + return; + } + + // Create new BLAS for added or changed meshes + let blas_resources = extracted_meshes + .extracted + .iter() + .filter(|(_, mesh)| is_mesh_raytracing_compatible(mesh)) + .map(|(asset_id, _)| { + let vertex_slice = mesh_allocator.mesh_vertex_slice(asset_id).unwrap(); + let index_slice = mesh_allocator.mesh_index_slice(asset_id).unwrap(); + + let (blas, blas_size) = + allocate_blas(&vertex_slice, &index_slice, asset_id, &render_device); + + blas_manager.insert(*asset_id, blas); + + (*asset_id, vertex_slice, index_slice, blas_size) + }) + .collect::>(); + + // Build geometry into each BLAS + let build_entries = blas_resources + .iter() + .map(|(asset_id, vertex_slice, index_slice, blas_size)| { + let geometry = BlasTriangleGeometry { + size: blas_size, + vertex_buffer: vertex_slice.buffer, + first_vertex: vertex_slice.range.start, + vertex_stride: 48, + index_buffer: Some(index_slice.buffer), + first_index: Some(index_slice.range.start), + transform_buffer: None, + transform_buffer_offset: None, + }; + BlasBuildEntry { + blas: &blas_manager[asset_id], + geometry: BlasGeometries::TriangleGeometries(vec![geometry]), + } + }) + .collect::>(); + + let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("build_blas_command_encoder"), + }); + command_encoder.build_acceleration_structures(&build_entries, &[]); + render_queue.submit([command_encoder.finish()]); +} + +fn allocate_blas( + vertex_slice: &MeshBufferSlice, + index_slice: &MeshBufferSlice, + asset_id: &AssetId, + render_device: &RenderDevice, +) -> (Blas, BlasTriangleGeometrySizeDescriptor) { + let blas_size = BlasTriangleGeometrySizeDescriptor { + vertex_format: Mesh::ATTRIBUTE_POSITION.format, + vertex_count: vertex_slice.range.len() as u32, + index_format: Some(IndexFormat::Uint32), + index_count: Some(index_slice.range.len() as u32), + flags: AccelerationStructureGeometryFlags::OPAQUE, + }; + + let blas = render_device.wgpu_device().create_blas( + &CreateBlasDescriptor { + label: Some(&asset_id.to_string()), + flags: AccelerationStructureFlags::PREFER_FAST_TRACE, + update_mode: AccelerationStructureUpdateMode::Build, + }, + BlasGeometrySizeDescriptors::Triangles { + descriptors: vec![blas_size.clone()], + }, + ); + + (blas, blas_size) +} + +fn is_mesh_raytracing_compatible(mesh: &Mesh) -> bool { + let triangle_list = mesh.primitive_topology() == PrimitiveTopology::TriangleList; + let vertex_attributes = mesh.attributes().map(|(attribute, _)| attribute.id).eq([ + Mesh::ATTRIBUTE_POSITION.id, + Mesh::ATTRIBUTE_NORMAL.id, + Mesh::ATTRIBUTE_UV_0.id, + Mesh::ATTRIBUTE_TANGENT.id, + ]); + let indexed_32 = matches!(mesh.indices(), Some(Indices::U32(..))); + mesh.enable_raytracing && triangle_list && vertex_attributes && indexed_32 +} diff --git a/crates/bevy_solari/src/scene/extract.rs b/crates/bevy_solari/src/scene/extract.rs new file mode 100644 index 0000000000000..46b11ba2b4985 --- /dev/null +++ b/crates/bevy_solari/src/scene/extract.rs @@ -0,0 +1,45 @@ +use super::RaytracingMesh3d; +use bevy_asset::{AssetId, Assets}; +use bevy_derive::Deref; +use bevy_ecs::{ + resource::Resource, + system::{Commands, Query}, +}; +use bevy_pbr::{MeshMaterial3d, StandardMaterial}; +use bevy_platform::collections::HashMap; +use bevy_render::{extract_resource::ExtractResource, sync_world::RenderEntity, Extract}; +use bevy_transform::components::GlobalTransform; + +pub fn extract_raytracing_scene( + instances: Extract< + Query<( + RenderEntity, + &RaytracingMesh3d, + &MeshMaterial3d, + &GlobalTransform, + )>, + >, + mut commands: Commands, +) { + for (render_entity, mesh, material, transform) in &instances { + commands + .entity(render_entity) + .insert((mesh.clone(), material.clone(), *transform)); + } +} + +#[derive(Resource, Deref, Default)] +pub struct StandardMaterialAssets(HashMap, StandardMaterial>); + +impl ExtractResource for StandardMaterialAssets { + type Source = Assets; + + fn extract_resource(source: &Self::Source) -> Self { + Self( + source + .iter() + .map(|(asset_id, material)| (asset_id, material.clone())) + .collect(), + ) + } +} diff --git a/crates/bevy_solari/src/scene/mod.rs b/crates/bevy_solari/src/scene/mod.rs new file mode 100644 index 0000000000000..72aeac7c975ae --- /dev/null +++ b/crates/bevy_solari/src/scene/mod.rs @@ -0,0 +1,90 @@ +mod binder; +mod blas; +mod extract; +mod types; + +pub use binder::RaytracingSceneBindings; +pub use types::RaytracingMesh3d; + +use crate::SolariPlugin; +use bevy_app::{App, Plugin}; +use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_ecs::schedule::IntoScheduleConfigs; +use bevy_render::{ + extract_resource::ExtractResourcePlugin, + mesh::{ + allocator::{allocate_and_free_meshes, MeshAllocator}, + RenderMesh, + }, + render_asset::prepare_assets, + render_resource::{BufferUsages, Shader}, + renderer::RenderDevice, + ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use binder::prepare_raytracing_scene_bindings; +use blas::{prepare_raytracing_blas, BlasManager}; +use extract::{extract_raytracing_scene, StandardMaterialAssets}; +use tracing::warn; + +const RAYTRACING_SCENE_BINDINGS_SHADER_HANDLE: Handle = + weak_handle!("9a69dbce-d718-4d10-841b-4025b28b525c"); +const SAMPLING_SHADER_HANDLE: Handle = weak_handle!("a3dd800b-9eaa-4e2b-8576-80b14b521ae9"); + +pub struct RaytracingScenePlugin; + +impl Plugin for RaytracingScenePlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + RAYTRACING_SCENE_BINDINGS_SHADER_HANDLE, + "raytracing_scene_bindings.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + SAMPLING_SHADER_HANDLE, + "sampling.wgsl", + Shader::from_wgsl + ); + + app.register_type::(); + } + + fn finish(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + let render_device = render_app.world().resource::(); + let features = render_device.features(); + if !features.contains(SolariPlugin::required_wgpu_features()) { + warn!( + "RaytracingScenePlugin not loaded. GPU lacks support for required features: {:?}.", + SolariPlugin::required_wgpu_features().difference(features) + ); + return; + } + + app.add_plugins(ExtractResourcePlugin::::default()); + + let render_app = app.sub_app_mut(RenderApp); + + render_app + .world_mut() + .resource_mut::() + .extra_buffer_usages |= BufferUsages::BLAS_INPUT | BufferUsages::STORAGE; + + render_app + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems(ExtractSchedule, extract_raytracing_scene) + .add_systems( + Render, + ( + prepare_raytracing_blas + .in_set(RenderSystems::PrepareAssets) + .before(prepare_assets::) + .after(allocate_and_free_meshes), + prepare_raytracing_scene_bindings.in_set(RenderSystems::PrepareBindGroups), + ), + ); + } +} diff --git a/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl b/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl new file mode 100644 index 0000000000000..19db2f09bf9db --- /dev/null +++ b/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl @@ -0,0 +1,163 @@ +#define_import_path bevy_solari::scene_bindings + +struct InstanceGeometryIds { + vertex_buffer_id: u32, + vertex_buffer_offset: u32, + index_buffer_id: u32, + index_buffer_offset: u32, +} + +struct VertexBuffer { vertices: array } + +struct IndexBuffer { indices: array } + +struct PackedVertex { + a: vec4, + b: vec4, + tangent: vec4, +} + +struct Vertex { + position: vec3, + normal: vec3, + uv: vec2, + tangent: vec4, +} + +fn unpack_vertex(packed: PackedVertex) -> Vertex { + var vertex: Vertex; + vertex.position = packed.a.xyz; + vertex.normal = vec3(packed.a.w, packed.b.xy); + vertex.uv = packed.b.zw; + vertex.tangent = packed.tangent; + return vertex; +} + +struct Material { + base_color: vec4, + emissive: vec4, + base_color_texture_id: u32, + normal_map_texture_id: u32, + emissive_texture_id: u32, + _padding: u32, +} + +const TEXTURE_MAP_NONE = 0xFFFFFFFFu; + +struct LightSource { + kind: u32, // 1 bit for kind, 31 bits for extra data + id: u32, +} + +const LIGHT_SOURCE_KIND_EMISSIVE_MESH = 0u; +const LIGHT_SOURCE_KIND_DIRECTIONAL = 1u; + +struct DirectionalLight { + direction_to_light: vec3, + cos_theta_max: f32, + illuminance: vec4, +} + +@group(0) @binding(0) var vertex_buffers: binding_array; +@group(0) @binding(1) var index_buffers: binding_array; +@group(0) @binding(2) var textures: binding_array>; +@group(0) @binding(3) var samplers: binding_array; +@group(0) @binding(4) var materials: array; +@group(0) @binding(5) var tlas: acceleration_structure; +@group(0) @binding(6) var transforms: array>; +@group(0) @binding(7) var geometry_ids: array; +@group(0) @binding(8) var material_ids: array; // TODO: Store material_id in instance_custom_index instead? +@group(0) @binding(9) var light_sources: array; +@group(0) @binding(10) var directional_lights: array; + +const RAY_T_MIN = 0.0001; +const RAY_T_MAX = 100000.0; + +const RAY_NO_CULL = 0xFFu; + +fn trace_ray(ray_origin: vec3, ray_direction: vec3, ray_t_min: f32, ray_t_max: f32, ray_flag: u32) -> RayIntersection { + let ray = RayDesc(ray_flag, RAY_NO_CULL, ray_t_min, ray_t_max, ray_origin, ray_direction); + var rq: ray_query; + rayQueryInitialize(&rq, tlas, ray); + rayQueryProceed(&rq); + return rayQueryGetCommittedIntersection(&rq); +} + +fn sample_texture(id: u32, uv: vec2) -> vec3 { + return textureSampleLevel(textures[id], samplers[id], uv, 0.0).rgb; // TODO: Mipmap +} + +struct ResolvedMaterial { + base_color: vec3, + emissive: vec3, +} + +struct ResolvedRayHitFull { + world_position: vec3, + world_normal: vec3, + geometric_world_normal: vec3, + uv: vec2, + triangle_area: f32, + material: ResolvedMaterial, +} + +fn resolve_material(material: Material, uv: vec2) -> ResolvedMaterial { + var m: ResolvedMaterial; + + m.base_color = material.base_color.rgb; + if material.base_color_texture_id != TEXTURE_MAP_NONE { + m.base_color *= sample_texture(material.base_color_texture_id, uv); + } + + m.emissive = material.emissive.rgb; + if material.emissive_texture_id != TEXTURE_MAP_NONE { + m.emissive *= sample_texture(material.emissive_texture_id, uv); + } + + return m; +} + +fn resolve_ray_hit_full(ray_hit: RayIntersection) -> ResolvedRayHitFull { + let barycentrics = vec3(1.0 - ray_hit.barycentrics.x - ray_hit.barycentrics.y, ray_hit.barycentrics); + return resolve_triangle_data_full(ray_hit.instance_id, ray_hit.primitive_index, barycentrics); +} + +fn resolve_triangle_data_full(instance_id: u32, triangle_id: u32, barycentrics: vec3) -> ResolvedRayHitFull { + let instance_geometry_ids = geometry_ids[instance_id]; + let material_id = material_ids[instance_id]; + + let index_buffer = &index_buffers[instance_geometry_ids.index_buffer_id].indices; + let vertex_buffer = &vertex_buffers[instance_geometry_ids.vertex_buffer_id].vertices; + let material = materials[material_id]; + + let indices_i = (triangle_id * 3u) + vec3(0u, 1u, 2u) + instance_geometry_ids.index_buffer_offset; + let indices = vec3((*index_buffer)[indices_i.x], (*index_buffer)[indices_i.y], (*index_buffer)[indices_i.z]) + instance_geometry_ids.vertex_buffer_offset; + let vertices = array(unpack_vertex((*vertex_buffer)[indices.x]), unpack_vertex((*vertex_buffer)[indices.y]), unpack_vertex((*vertex_buffer)[indices.z])); + + let transform = transforms[instance_id]; + let local_position = mat3x3(vertices[0].position, vertices[1].position, vertices[2].position) * barycentrics; + let world_position = (transform * vec4(local_position, 1.0)).xyz; + + let uv = mat3x2(vertices[0].uv, vertices[1].uv, vertices[2].uv) * barycentrics; + + 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 Nt = sample_texture(material.normal_map_texture_id, uv); + world_normal = normalize(Nt.x * T + Nt.y * B + Nt.z * N); + } + + let triangle_edge0 = vertices[0].position - vertices[1].position; + let triangle_edge1 = vertices[0].position - vertices[2].position; + let triangle_area = length(cross(triangle_edge0, triangle_edge1)) / 2.0; + + let resolved_material = resolve_material(material, uv); + + return ResolvedRayHitFull(world_position, world_normal, geometric_world_normal, uv, triangle_area, resolved_material); +} diff --git a/crates/bevy_solari/src/scene/sampling.wgsl b/crates/bevy_solari/src/scene/sampling.wgsl new file mode 100644 index 0000000000000..9db1e3d3e4649 --- /dev/null +++ b/crates/bevy_solari/src/scene/sampling.wgsl @@ -0,0 +1,97 @@ +#define_import_path bevy_solari::sampling + +#import bevy_pbr::utils::{rand_f, rand_vec2f, rand_range_u} +#import bevy_render::maths::PI +#import bevy_solari::scene_bindings::{trace_ray, RAY_T_MIN, RAY_T_MAX, light_sources, directional_lights, LIGHT_SOURCE_KIND_DIRECTIONAL, resolve_triangle_data_full} + +// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec28%3A303 +fn sample_cosine_hemisphere(normal: vec3, rng: ptr) -> vec3 { + 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)); + let x = normal.x + sin_theta * cos(phi); + let y = normal.y + sin_theta * sin(phi); + let z = normal.z + cos_theta; + return vec3(x, y, z); +} + +fn sample_random_light(ray_origin: vec3, origin_world_normal: vec3, rng: ptr) -> vec3 { + let light_count = arrayLength(&light_sources); + let light_id = rand_range_u(light_count, rng); + let light_source = light_sources[light_id]; + + var radiance: vec3; + if light_source.kind == LIGHT_SOURCE_KIND_DIRECTIONAL { + radiance = sample_directional_light(ray_origin, origin_world_normal, light_source.id, rng); + } else { + radiance = sample_emissive_mesh(ray_origin, origin_world_normal, light_source.id, light_source.kind >> 1u, rng); + } + + let inverse_pdf = f32(light_count); + + return radiance * inverse_pdf; +} + +fn sample_directional_light(ray_origin: vec3, origin_world_normal: vec3, directional_light_id: u32, rng: ptr) -> vec3 { + let directional_light = directional_lights[directional_light_id]; + + // Sample a random direction within a cone whose base is the sun approximated as a disk + // https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec30%3A305 + let random = rand_vec2f(rng); + let cos_theta = (1.0 - random.x) + random.x * directional_light.cos_theta_max; + let sin_theta = sqrt(1.0 - cos_theta * cos_theta); + let phi = random.y * 2.0 * PI; + let x = cos(phi) * sin_theta; + let y = sin(phi) * sin_theta; + var ray_direction = vec3(x, y, cos_theta); + + // Rotate the ray so that the cone it was sampled from is aligned with the light direction + ray_direction = build_orthonormal_basis(directional_light.direction_to_light) * ray_direction; + + let ray_hit = trace_ray(ray_origin, ray_direction, RAY_T_MIN, RAY_T_MAX, RAY_FLAG_TERMINATE_ON_FIRST_HIT); + let visibility = f32(ray_hit.kind == RAY_QUERY_INTERSECTION_NONE); + + let cos_theta_origin = saturate(dot(ray_direction, origin_world_normal)); + + // No need to divide by the pdf, because we also need to divide by the solid angle to convert from illuminance to luminance, and they cancel out + return directional_light.illuminance.rgb * visibility * cos_theta_origin; +} + +fn sample_emissive_mesh(ray_origin: vec3, origin_world_normal: vec3, instance_id: u32, triangle_count: u32, rng: ptr) -> vec3 { + let barycentrics = sample_triangle_barycentrics(rng); + let triangle_id = rand_range_u(triangle_count, rng); + + let triangle_data = resolve_triangle_data_full(instance_id, triangle_id, barycentrics); + + let light_distance = distance(ray_origin, triangle_data.world_position); + let ray_direction = (triangle_data.world_position - ray_origin) / light_distance; + let cos_theta_origin = saturate(dot(ray_direction, origin_world_normal)); + let cos_theta_light = saturate(dot(-ray_direction, triangle_data.world_normal)); + let light_distance_squared = light_distance * light_distance; + + let ray_hit = trace_ray(ray_origin, ray_direction, RAY_T_MIN, light_distance - RAY_T_MIN - RAY_T_MIN, RAY_FLAG_TERMINATE_ON_FIRST_HIT); + let visibility = f32(ray_hit.kind == RAY_QUERY_INTERSECTION_NONE); + + let radiance = triangle_data.material.emissive.rgb * visibility * cos_theta_origin * (cos_theta_light / light_distance_squared); + + let inverse_pdf = f32(triangle_count) * triangle_data.triangle_area; + + return radiance * inverse_pdf; +} + +// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec22%3A297 +fn sample_triangle_barycentrics(rng: ptr) -> vec3 { + var barycentrics = rand_vec2f(rng); + if barycentrics.x + barycentrics.y > 1.0 { barycentrics = 1.0 - barycentrics; } + return vec3(1.0 - barycentrics.x - barycentrics.y, barycentrics); +} + +// https://jcgt.org/published/0006/01/01/paper.pdf +fn build_orthonormal_basis(normal: vec3) -> mat3x3 { + let sign = select(-1.0, 1.0, normal.z >= 0.0); + let a = -1.0 / (sign + normal.z); + let b = normal.x * normal.y * a; + let tangent = vec3(1.0 + sign * normal.x * normal.x * a, sign * b, -sign * normal.x); + let bitangent = vec3(b, sign + normal.y * normal.y * a, -normal.y); + return mat3x3(tangent, bitangent, normal); +} diff --git a/crates/bevy_solari/src/scene/types.rs b/crates/bevy_solari/src/scene/types.rs new file mode 100644 index 0000000000000..8ee33b31fce8a --- /dev/null +++ b/crates/bevy_solari/src/scene/types.rs @@ -0,0 +1,21 @@ +use bevy_asset::Handle; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{component::Component, prelude::ReflectComponent}; +use bevy_mesh::Mesh; +use bevy_pbr::{MeshMaterial3d, StandardMaterial}; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_render::sync_world::SyncToRenderWorld; +use bevy_transform::components::Transform; +use derive_more::derive::From; + +/// A mesh component used for raytracing. +/// +/// The mesh used in this component must have [`bevy_render::mesh::Mesh::enable_raytracing`] set to true, +/// use the following set of vertex attributes: `{POSITION, NORMAL, UV_0, TANGENT}`, use [`bevy_render::render_resource::PrimitiveTopology::TriangleList`], +/// and use [`bevy_mesh::Indices::U32`]. +/// +/// The material used for this entity must be [`MeshMaterial3d`]. +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[reflect(Component, Default, Clone, PartialEq)] +#[require(MeshMaterial3d, Transform, SyncToRenderWorld)] +pub struct RaytracingMesh3d(pub Handle); diff --git a/docs/cargo_features.md b/docs/cargo_features.md index f15fa1c4c6683..0143457152af7 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -68,6 +68,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_dev_tools|Provides a collection of developer tools| |bevy_image|Load and access image data. Usually added by an image format| |bevy_remote|Enable the Bevy Remote Protocol| +|bevy_solari|Provides raytraced lighting (do not use, not yet ready for users)| |bevy_ui_debug|Provides a debug overlay for bevy UI| |bmp|BMP image format support| |configurable_error_handler|Use the configurable global error handler as the default error handler.| diff --git a/examples/3d/solari.rs b/examples/3d/solari.rs new file mode 100644 index 0000000000000..30def5bb1afea --- /dev/null +++ b/examples/3d/solari.rs @@ -0,0 +1,83 @@ +//! Demonstrates realtime dynamic global illumination rendering using Bevy Solari. + +#[path = "../helpers/camera_controller.rs"] +mod camera_controller; + +use bevy::{ + prelude::*, + render::{camera::CameraMainTextureUsages, mesh::Indices, render_resource::TextureUsages}, + scene::SceneInstanceReady, + solari::{ + pathtracer::Pathtracer, + prelude::{RaytracingMesh3d, SolariPlugin}, + }, +}; +use camera_controller::{CameraController, CameraControllerPlugin}; +use std::f32::consts::PI; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, SolariPlugin, CameraControllerPlugin)) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands, asset_server: Res) { + commands + .spawn(SceneRoot( + asset_server.load("models/CornellBox/box_modified.glb#Scene0"), + )) + .observe(add_raytracing_meshes_on_scene_load); + + commands.spawn(( + DirectionalLight { + illuminance: light_consts::lux::FULL_DAYLIGHT, + shadows_enabled: true, + ..default() + }, + Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, PI * -0.43, PI * -0.08, 0.0)), + )); + + commands.spawn(( + Camera3d::default(), + Camera { + hdr: true, + clear_color: ClearColorConfig::Custom(Color::BLACK), + ..default() + }, + CameraController::default(), + Pathtracer::default(), + CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING), + Transform::from_matrix(Mat4 { + x_axis: Vec4::new(0.99480534, 0.0, -0.10179563, 0.0), + y_axis: Vec4::new(-0.019938117, 0.98063105, -0.19484669, 0.0), + z_axis: Vec4::new(0.09982395, 0.19586414, 0.975537, 0.0), + w_axis: Vec4::new(0.68394995, 2.2785425, 6.68395, 1.0), + }), + )); +} + +fn add_raytracing_meshes_on_scene_load( + trigger: Trigger, + children: Query<&Children>, + mesh: Query<&Mesh3d>, + mut meshes: ResMut>, + mut commands: Commands, +) { + for (_, mesh) in meshes.iter_mut() { + if let Some(indices) = mesh.indices_mut() { + if let Indices::U16(u16_indices) = indices { + *indices = Indices::U32(u16_indices.iter().map(|i| *i as u32).collect()); + } + } + } + + for descendant in children.iter_descendants(trigger.target()) { + if let Ok(mesh) = mesh.get(descendant) { + commands + .entity(descendant) + .insert(RaytracingMesh3d(mesh.0.clone())) + .remove::(); + } + } +} diff --git a/examples/README.md b/examples/README.md index 060683f96d891..161fb8578cadf 100644 --- a/examples/README.md +++ b/examples/README.md @@ -184,6 +184,7 @@ Example | Description [Shadow Biases](../examples/3d/shadow_biases.rs) | Demonstrates how shadow biases affect shadows in a 3d scene [Shadow Caster and Receiver](../examples/3d/shadow_caster_receiver.rs) | Demonstrates how to prevent meshes from casting/receiving shadows in a 3d scene [Skybox](../examples/3d/skybox.rs) | Load a cubemap texture onto a cube like a skybox and cycle through different compressed texture formats. +[Solari](../examples/3d/solari.rs) | Demonstrates realtime dynamic global illumination rendering using Bevy Solari. [Specular Tint](../examples/3d/specular_tint.rs) | Demonstrates specular tints and maps [Spherical Area Lights](../examples/3d/spherical_area_lights.rs) | Demonstrates how point light radius values affect light behavior [Split Screen](../examples/3d/split_screen.rs) | Demonstrates how to render two cameras to the same window to accomplish "split screen" diff --git a/examples/helpers/camera_controller.rs b/examples/helpers/camera_controller.rs index 07f0f31b1108b..a60aa69a5ea9a 100644 --- a/examples/helpers/camera_controller.rs +++ b/examples/helpers/camera_controller.rs @@ -198,7 +198,7 @@ fn run_camera_controller( } let cursor_grab = *mouse_cursor_grab || *toggle_cursor_grab; - // Apply movement update + // Update velocity if axis_input != Vec3::ZERO { let max_speed = if key_input.pressed(controller.key_run) { controller.run_speed @@ -213,11 +213,15 @@ fn run_camera_controller( controller.velocity = Vec3::ZERO; } } - let forward = *transform.forward(); - let right = *transform.right(); - transform.translation += controller.velocity.x * dt * right - + controller.velocity.y * dt * Vec3::Y - + controller.velocity.z * dt * forward; + + // Apply movement update + if controller.velocity != Vec3::ZERO { + let forward = *transform.forward(); + let right = *transform.right(); + transform.translation += controller.velocity.x * dt * right + + controller.velocity.y * dt * Vec3::Y + + controller.velocity.z * dt * forward; + } // Handle cursor grab if cursor_grab_change { diff --git a/release-content/release-notes/bevy_solari.md b/release-content/release-notes/bevy_solari.md new file mode 100644 index 0000000000000..79893315bb35e --- /dev/null +++ b/release-content/release-notes/bevy_solari.md @@ -0,0 +1,32 @@ +--- +title: Initial raytraced lighting progress (bevy_solari) +authors: ["@JMS55"] +pull_requests: [19058] +--- + +(TODO: Embed solari example screenshot here) + +In Bevy 0.17, we've made the first steps towards realtime raytraced lighting in the form of the new bevy_solari crate. + +For some background, lighting in video games can be split into two parts: direct and indirect lighting. + +Direct lighting is light that that is emitted from a light source, bounces off of one surface, and then reaches the camera. Indirect lighting by contrast is light that bounces off of different surfaces many times before reaching the camera, and is often called global illumination. + +(TODO: Diagrams of direct vs indirect light) + +In Bevy, direct lighting comes from analytical light components (`DirectionalLight`, `PointLight`, `SpotLight`) and shadow maps. Indirect lighting comes from a hardcoded `AmbientLight`, baked lighting components (`EnvironmentMapLight`, `IrradianceVolume`, `Lightmap`), and screen-space calculations (`ScreenSpaceAmbientOcclusion`, `ScreenSpaceReflections`, `specular_transmission`, `diffuse_transmission`). + +The problem with these methods is that they all have large downsides: + +* Emissive meshes do not cast light onto other objects, either direct or indirect. +* Shadow maps are very expensive to render and consume a lot of memory, so you're limited to using only a few shadow casting lights. Good quality can be difficult to obtain in large scenes. +* Baked lighting does not update in realtime as objects and lights move around, is low resolution/quality, and requires time to bake, slowing down game production. +* Screen-space methods have low quality and do not capture off-screen geometry and light. + +Bevy Solari is intended as a completely alternate, high-end lighting solution for Bevy that uses GPU-accelerated raytracing to fix all of the above problems. Emissive meshes will properly cast light and shadows, you will be able to have hundreds of shadow casting lights, quality will be much better, it will require no baking time, and it will support _fully_ dynamic scenes! + +While Bevy 0.17 adds the bevy_solari crate, it's intended as a long-term project. Currently there is only a non-realtime path tracer intended as a reference and testbed for developing Bevy Solari. There is nothing usable yet for game developers. However, feel free to run the solari example to see the path tracer is action, and look forwards to more work on Bevy Solari in future releases! (TODO: Is this burying the lede?) + +(TODO: Embed bevy_solari logo here, or somewhere else that looks good) + +Special thanks to @Vecvec for adding raytracing support to wgpu.